From f1cd50855e329bffa9971827b202a1ef113ba308 Mon Sep 17 00:00:00 2001 From: "GitHub Actions (Docs Sync)" Date: Tue, 3 Mar 2026 02:09:46 +0000 Subject: [PATCH 1/7] docs: sync shared documentation from PurposePath_Api [docs-sync] Synced from: PurposePath_Api@2d433dbe Original commit: Merge pull request #694 from mottych/docs/sync-dev Original author: Motty Chen Branch: master [skip ci] --- ...ssue-667-schema-diff-and-migration-plan.md | 99 + .../measure-integration-requirements.md | 407 +++ docs/shared/Specifications/README.md | 21 +- .../admin_ai_specifications.md | 2294 ++++++++--------- .../backend-integration-unified-ai.md | 0 .../coaching-session-workflow.md | 190 +- .../ai-fe/measure-integration.md | 362 +++ .../admin-api-specification.md | 0 .../integration-admin-api-specification.md | 365 +++ .../Websocket/REALTIME_HOOKS_USAGE.md | 0 .../REALTIME_WEBSOCKET_IMPLEMENTATION.md | 0 .../Websocket/SSE_TO_WEBSOCKET_MIGRATION.md | 0 .../Websocket/WEBSOCKET_EPIC_SUMMARY.md | 0 .../WEBSOCKET_FRONTEND_INTEGRATION_SPEC.md | 0 .../WEBSOCKET_IMPLEMENTATION_PROGRESS.md | 0 .../Websocket/WEBSOCKET_MIGRATION_ANALYSIS.md | 0 .../Websocket/WEBSOCKET_SESSION_RECOVERY.md | 0 .../Websocket/WEBSOCKET_UPDATE_EXAMPLE.md | 0 .../Websocket/WEBSOCKET_UPDATE_PROTOCOL.md | 0 .../{user-app => api-fe}/account-api.md | 0 .../{user-app => api-fe}/account-gap.md | 0 .../business-foundation-api.md | 14 +- .../{user-app => api-fe}/common-patterns.md | 0 .../{user-app => api-fe}/dashboard-service.md | 0 .../{user-app => api-fe}/index.md | 19 +- .../org-structure-service.md | 1586 +++++++++++- .../traction-service/README.md | 9 + .../traction-service/actions-api.md | 0 .../traction-service/alignment-api.md | 0 .../dashboard-reports-activities-api.md | 0 .../traction-service/goals-api.md | 0 .../traction-service/insights-api.md | 0 .../traction-service/integration-service.md | 457 ++++ .../traction-service/issues-api.md | 0 .../traction-service/measure-data-api.md | 0 .../traction-service/measure-links-api.md | 0 .../traction-service/measures-api.md | 29 + .../traction-service/strategies-api.md | 0 .../async-coaching-message-events.md | 0 .../async-integration-ai-cdata-contracts.md | 192 ++ .../Specifications/user-app/people-service.md | 1488 ----------- 41 files changed, 4747 insertions(+), 2785 deletions(-) create mode 100644 docs/shared/Requirements/measure-integration/issue-667-schema-diff-and-migration-plan.md create mode 100644 docs/shared/Requirements/measure-integration/measure-integration-requirements.md rename docs/shared/Specifications/{ai-admin-portal => ai-admin}/admin_ai_specifications.md (96%) rename docs/shared/Specifications/{ai-user => ai-fe}/backend-integration-unified-ai.md (100%) rename docs/shared/Specifications/{ai-user => ai-fe}/coaching-session-workflow.md (96%) create mode 100644 docs/shared/Specifications/ai-fe/measure-integration.md rename docs/shared/Specifications/{admin-portal => api-admin}/admin-api-specification.md (100%) create mode 100644 docs/shared/Specifications/api-admin/integration-admin-api-specification.md rename docs/shared/Specifications/{user-app => api-fe}/Websocket/REALTIME_HOOKS_USAGE.md (100%) rename docs/shared/Specifications/{user-app => api-fe}/Websocket/REALTIME_WEBSOCKET_IMPLEMENTATION.md (100%) rename docs/shared/Specifications/{user-app => api-fe}/Websocket/SSE_TO_WEBSOCKET_MIGRATION.md (100%) rename docs/shared/Specifications/{user-app => api-fe}/Websocket/WEBSOCKET_EPIC_SUMMARY.md (100%) rename docs/shared/Specifications/{user-app => api-fe}/Websocket/WEBSOCKET_FRONTEND_INTEGRATION_SPEC.md (100%) rename docs/shared/Specifications/{user-app => api-fe}/Websocket/WEBSOCKET_IMPLEMENTATION_PROGRESS.md (100%) rename docs/shared/Specifications/{user-app => api-fe}/Websocket/WEBSOCKET_MIGRATION_ANALYSIS.md (100%) rename docs/shared/Specifications/{user-app => api-fe}/Websocket/WEBSOCKET_SESSION_RECOVERY.md (100%) rename docs/shared/Specifications/{user-app => api-fe}/Websocket/WEBSOCKET_UPDATE_EXAMPLE.md (100%) rename docs/shared/Specifications/{user-app => api-fe}/Websocket/WEBSOCKET_UPDATE_PROTOCOL.md (100%) rename docs/shared/Specifications/{user-app => api-fe}/account-api.md (100%) rename docs/shared/Specifications/{user-app => api-fe}/account-gap.md (100%) rename docs/shared/Specifications/{user-app => api-fe}/business-foundation-api.md (98%) rename docs/shared/Specifications/{user-app => api-fe}/common-patterns.md (100%) rename docs/shared/Specifications/{user-app => api-fe}/dashboard-service.md (100%) rename docs/shared/Specifications/{user-app => api-fe}/index.md (94%) rename docs/shared/Specifications/{user-app => api-fe}/org-structure-service.md (68%) rename docs/shared/Specifications/{user-app => api-fe}/traction-service/README.md (93%) rename docs/shared/Specifications/{user-app => api-fe}/traction-service/actions-api.md (100%) rename docs/shared/Specifications/{user-app => api-fe}/traction-service/alignment-api.md (100%) rename docs/shared/Specifications/{user-app => api-fe}/traction-service/dashboard-reports-activities-api.md (100%) rename docs/shared/Specifications/{user-app => api-fe}/traction-service/goals-api.md (100%) rename docs/shared/Specifications/{user-app => api-fe}/traction-service/insights-api.md (100%) create mode 100644 docs/shared/Specifications/api-fe/traction-service/integration-service.md rename docs/shared/Specifications/{user-app => api-fe}/traction-service/issues-api.md (100%) rename docs/shared/Specifications/{user-app => api-fe}/traction-service/measure-data-api.md (100%) rename docs/shared/Specifications/{user-app => api-fe}/traction-service/measure-links-api.md (100%) rename docs/shared/Specifications/{user-app => api-fe}/traction-service/measures-api.md (96%) rename docs/shared/Specifications/{user-app => api-fe}/traction-service/strategies-api.md (100%) rename docs/shared/Specifications/{eventbridge => integration}/async-coaching-message-events.md (100%) create mode 100644 docs/shared/Specifications/integration/async-integration-ai-cdata-contracts.md delete mode 100644 docs/shared/Specifications/user-app/people-service.md diff --git a/docs/shared/Requirements/measure-integration/issue-667-schema-diff-and-migration-plan.md b/docs/shared/Requirements/measure-integration/issue-667-schema-diff-and-migration-plan.md new file mode 100644 index 00000000..73b03a8b --- /dev/null +++ b/docs/shared/Requirements/measure-integration/issue-667-schema-diff-and-migration-plan.md @@ -0,0 +1,99 @@ +# Issue #667 - Schema Diff and Migration/Backfill Plan + +## Scope +This document captures the implemented schema/model delta for #667 and the migration/backfill approach for existing records. + +## 1) Schema Diff (Existing -> Target) + +### MeasureCatalog +- Added `business_definition` (nullable) + - Domain: `MeasureCatalog.BusinessDefinition` + - Data model: `MeasureCatalogDataModel.BusinessDefinition` + +### Measure +- Added `business_definition_override` (nullable) + - Domain: `Measure.BusinessDefinitionOverride` + - Data model: `MeasureDataModel.BusinessDefinitionOverride` + +### MeasureIntegration +- Added SQL lifecycle metadata (all nullable unless noted): + - `definition_version` + - `definition_hash` + - `previous_definition_version` + - `previous_definition_hash` + - `requires_template_regeneration` (bool, default false) + - `regeneration_reason_code` + - `sql_template_hash` + - `query_artifact_ref` + - `sql_template_generation_status` (string enum, default `Pending`) + - `last_sql_template_generated_at` + - `last_generation_error_code` + - `last_generation_error_stage` + - `last_generation_error_message` + +### ConnectionConfiguration +- Added external reference model fields: + - `external_provider` + - `external_connection_id` + - `workspace_context` +- Added validation diagnostics: + - `last_validation_error` + +### SystemMeasureConfig +- Added intent/constraint metadata: + - `ai_topic` + - `system_constraints_json` + +## 2) Index/GSI Impact Assessment +- No new query paths were introduced in #667 that require immediate GSI additions. +- Existing GSIs remain valid: + - `purposepath-system-measure-configs`: `system-index`, `measure-catalog-index` + - `purposepath-connection-configurations`: `tenant-index`, `system-index` + - `purposepath-measure-integrations`: `measure-index`, `connection-index`, `tenant-index` +- Pulumi updates are **not required** for this slice because DynamoDB is schemaless for non-key attributes and no new key/index access patterns were added. + +## 3) Migration and Backfill Plan + +### 3.1 Safety +- Do not delete or rewrite legacy credential fields in-place. +- Keep old fields readable while new external-reference fields are populated. +- Run migration in idempotent batches by tenant. + +### 3.2 Backfill Steps +1. `MeasureCatalog` + - For records with empty `business_definition`, leave null. + - Optional enrichment can be done later from curated catalog metadata. + +2. `Measure` + - Set `business_definition_override` only when explicit tenant/measure overrides exist. + - Default remains null. + +3. `MeasureIntegration` + - Initialize defaults when fields are missing: + - `requires_template_regeneration = false` + - `sql_template_generation_status = Pending` + - Keep lifecycle hashes/versions null until first template generation flow runs. + +4. `ConnectionConfiguration` + - Legacy records with encrypted credentials: + - retain `encrypted_credentials` unchanged + - set `external_provider` / `external_connection_id` only when mapping to known CData references is available + - Non-mappable legacy records: + - set `validation_status = Invalid` + - populate `last_validation_error` with remediation hint + +5. `SystemMeasureConfig` + - Initialize `ai_topic` and `system_constraints_json` as null for existing records. + - Populate incrementally during system-specific configuration updates. + +### 3.3 Verification +- Build and domain tests must pass after migration tools/scripts are introduced. +- Spot-check by tenant: + - query active connections and verify external-reference fields are either valid or explicitly flagged. + - query measure integrations and verify lifecycle defaults are present. + +## 4) Rollout Notes +- Deploy code first (backward compatible). +- Run backfill in dev stack. +- Validate read paths and integration execution behavior. +- Promote to higher environments after verification. diff --git a/docs/shared/Requirements/measure-integration/measure-integration-requirements.md b/docs/shared/Requirements/measure-integration/measure-integration-requirements.md new file mode 100644 index 00000000..9fa338cf --- /dev/null +++ b/docs/shared/Requirements/measure-integration/measure-integration-requirements.md @@ -0,0 +1,407 @@ +# Measure Integration - Requirements Document + +**Version:** 1.0 +**Last Updated:** February 17, 2026 +**Status:** Draft for Validation +**Ownership Model:** Shared between PurposePath backend project and AI backend project +**Primary Scope:** Contract and architecture requirements for AI-driven SQL template generation and measure integration lifecycle + +--- + +## Table of Contents + +1. [Intent and Objectives](#1-intent-and-objectives) +2. [Scope](#2-scope) +3. [Source Artifacts and Traceability](#3-source-artifacts-and-traceability) +4. [Architecture Requirements](#4-architecture-requirements) +5. [Contract Requirements (AI ↔ Integration)](#5-contract-requirements-ai--integration) +6. [Functional Requirements](#6-functional-requirements) +7. [Security Requirements](#7-security-requirements) +8. [Performance and Reliability Requirements](#8-performance-and-reliability-requirements) +9. [Cost Estimate](#9-cost-estimate) +10. [Completion and Validation Checklist](#10-completion-and-validation-checklist) +11. [Shared Delivery Governance](#11-shared-delivery-governance) + +--- + +## 1. Intent and Objectives + +### 1.1 Intent +Establish a clear, testable, and contract-first integration model between PurposePath and AI services for SQL template generation, measure data retrieval orchestration, and deterministic publication of measure outcomes. + +### 1.2 Objectives +1. Define a stable, versioned event contract between systems. +2. Ensure deterministic and idempotent processing from generation request to terminal outcome. +3. Separate business intent, system constraints, and runtime lifecycle responsibilities. +4. Improve reliability, observability, and governance without coupling to implementation details. +5. Provide measurable completion criteria aligned with epic and issue scope. + +--- + +## 2. Scope + +### 2.1 In Scope +- Contract requirements for EventBridge events used for SQL template generation. +- Requirements for intent resolution hierarchy and parameter semantics. +- Requirements for idempotency, terminal outcome guarantees, and replay safety. +- Security, performance, and operational design constraints. +- Cost model and expected cost controls. + +### 2.2 Out of Scope +- Internal code structure, class design, and framework-specific implementation. +- Infrastructure-as-code templates or deployment runbooks. +- UI behavior details. +- Non-integration AI topics unrelated to SQL template generation. + +--- + +## 3. Source Artifacts and Traceability + +This requirements document consolidates scope and decisions from the following planning artifacts: + +- Epic #665: Measure integration refactor epic +- Issue #666: Architecture and contract baseline +- Issue #667: Schema and data model changes +- Issue #668: Connection model and provider-facing metadata alignment +- Issue #669: Payload and event contract implementation scope +- Issue #670: Worker/orchestration behavior and idempotency +- Issue #671: API/EventBridge specification alignment +- Issue #672: Legacy cleanup and deprecation scope +- Issue #673: Validation, rollout, and completion criteria + +> Requirement IDs in this document map to one or more of these issues for completion tracking. + +This document is also a shared contract artifact for the AI backend project. The AI project may track equivalent scope with separate issue IDs in a different repository. + +**Traceability rule:** requirement completion is considered final only when linked backend-side and AI-side tracking artifacts are both marked complete. + +--- + +## 4. Architecture Requirements + +### 4.1 Architectural Principles +**AR-001** Contract-first design: all cross-system interactions must be defined by versioned contracts prior to release. + +**AR-002** Separation of concerns: +- business intent definition +- system-specific constraints +- runtime lifecycle and state +must remain logically distinct. + +**AR-003** Deterministic processing: identical inputs and definition versions must produce reproducible contract outcomes. + +**AR-004** Backward-compatible evolution: contract changes must be versioned and non-breaking by default. + +### 4.2 Logical Architecture Boundaries +**AR-010** The integration domain must expose a dedicated contract surface for SQL template generation events. + +**AR-011** AI systems must consume request events and emit exactly one terminal event per generation cycle. + +**AR-012** Runtime state and terminal outcome identity must be keyed by a stable generation identity. + +**AR-013** Solution design must conform to `.github/DEVELOPMENT_GUIDELINES.md` architectural boundaries: +- controllers remain boundary-only and delegate via MediatR +- domain layer remains pure and free of infrastructure concerns +- contracts/interfaces required by domain are defined in domain-facing boundaries +- cross-layer dependencies must follow Clean Architecture direction + +**AR-014** Any architecture deviation from `.github/DEVELOPMENT_GUIDELINES.md` must be explicitly documented, risk-assessed, and approved before release. + +### 4.3 Intent and Parameter Architecture +**AR-020** Intent precedence must be: +1. measure-level override intent (when present) +2. catalog-level canonical intent + +**AR-021** System-level configuration defines allowed parameter schema and constraints, not business intent text. + +**AR-022** Runtime integration state owns lifecycle metadata (definition version/hash, regeneration state, generation outcomes). + +--- + +## 5. Contract Requirements (AI ↔ Integration) + +### 5.1 Contract Version and Event Set +**CR-001** Contract version 1.2 is the baseline for inter-system event exchange in scope. + +**CR-002** Required event types: +- integration.sql.template.generate.requested +- integration.sql.template.generate.completed +- integration.sql.template.generate.failed + +### 5.2 Envelope and Identity +**CR-010** Every event must contain the standard EventBridge envelope fields. + +**CR-011** Every event detail must include a common identity block containing event version, provider, correlation identity, generation identity, tenant identity, integration identity, idempotency key, and canonical definition version/hash. + +**CR-012** Definition identity must exist in a single canonical location in the payload. + +### 5.3 Requested Event Requirements +**CR-020** Requested events must provide: +- resolved intent template and period-window strategy +- source/system connection context needed for contract execution +- allowed parameter schema and selected parameter values +- regeneration flag +- SQL policy constraints, including required placeholder style and max rows semantics + +**CR-021** The contract must define max rows as final result-set row count. + +### 5.4 Completed Event Requirements +**CR-030** Completed events must provide: +- success status +- parameterized SQL template +- template hash +- binding schema +- explicit applied parameters list +- explicit ignored parameters list with enum reason +- validation outcome data +- generation timing metadata + +**CR-031** Completed events must include provider and traceability connection fields for downstream diagnostics. + +### 5.5 Failed Event Requirements +**CR-040** Failed events must provide: +- failed status +- stable error code and error stage enums +- retryability flag +- sanitized failure message +- attempt and duration fields + +**CR-041** Failed events must support optional retry-after, provider error code, and structured validation failures. + +### 5.6 Enum Governance +**CR-050** Error codes, error stages, ignored parameter reasons, and validation failure codes must be governed by canonical enums. + +**CR-051** Enum changes require versioned contract governance and compatibility review. + +### 5.7 Compatibility Rules +**CR-060** Terminal events must return the same definition version/hash from the requested event. + +**CR-061** Runtime binding expectations (if present) must be compatible with emitted parameter binding schema. + +**CR-062** SQL placeholder style in emitted template must match policy-defined style. + +--- + +## 6. Functional Requirements + +### 6.1 Lifecycle and Regeneration +**FR-001** The system must initiate SQL template generation when structural query-shaping changes are detected. + +**FR-002** Value-only parameter changes must not require template regeneration by default. + +**FR-003** Regeneration state must be explicit in request contract. + +### 6.2 Idempotency and Terminal Behavior +**FR-010** At most one terminal event may exist for a generation identity. + +**FR-011** Exactly one terminal outcome (completed or failed) must be produced per generation identity. + +**FR-012** Duplicate or replayed terminal events must be safely recognized and handled without data corruption. + +### 6.3 Validation and Policy Enforcement +**FR-020** Completion is valid only when policy and validation checks pass. + +**FR-021** SQL templates must remain parameterized and must not interpolate user-provided values. + +**FR-022** Forbidden operation policy must be enforced before successful terminal publication. + +### 6.4 Traceability and Auditability +**FR-030** Contract events must support end-to-end correlation via correlation and generation identities. + +**FR-031** Contract events must include sufficient context for post-incident diagnosis without requiring implementation-specific logs. + +--- + +## 7. Security Requirements + +### 7.1 Data Protection +**SR-001** Event payloads and logs must not expose credentials, secrets, or raw sensitive tokens. + +**SR-002** Failure diagnostics must include only sanitized fields approved for cross-system transport. + +### 7.2 Access and Trust Boundaries +**SR-010** Producer and consumer trust boundaries must be explicit and validated through contract version controls. + +**SR-011** Event sources and detail types must be constrained to authorized producer identities. + +**SR-012** Tenant isolation is mandatory: every contract event and lifecycle transition must be scoped and validated by tenant identity so cross-tenant access, processing, or publication cannot occur. + +**SR-013** Tenant identity used for processing must be consistent across request and terminal events; identity mismatch must result in rejection. + +### 7.3 Integrity and Non-Repudiation +**SR-020** Definition hash and template hash must be used for integrity checks across systems. + +**SR-021** Terminal event acceptance must verify identity consistency with originating request. + +--- + +## 8. Performance and Reliability Requirements + +### 8.1 Performance Targets +**PR-001** Generation workflows should complete within an operational target window appropriate for asynchronous orchestration (target: low-seconds to low-minutes under nominal load). + +**PR-002** Contract validation overhead should remain small relative to total generation latency. + +### 8.2 Reliability Targets +**PR-010** Terminal event delivery reliability target: 99.9%+ successful terminal publication under normal operating conditions. + +**PR-011** Transient failure handling must support bounded retries with explicit retryability semantics. + +**PR-012** Non-retryable failures must fail fast with stable error taxonomy. + +### 8.3 Observability Requirements +**PR-020** Metrics and logs must support tracking for: +- generation throughput +- success/failure rates +- retry rates +- terminal latency distribution +- contract validation failures + +**PR-021** Observability signals must be keyed by generation and correlation identity. + +--- + +## 9. Cost Estimate + +### 9.1 Estimation Approach +This estimate is a planning-level range based on expected event volume, AI generation frequency, and validation overhead. It is not a billing commitment. + +### 9.2 Cost Drivers +1. Event traffic volume (request + terminal events) +2. AI generation and validation calls per generation cycle +3. Retry frequency on transient failures +4. Storage and retention for lifecycle metadata and audit traces +5. Observability ingestion and retention + +### 9.3 Relative Cost Profile (Planning) +- **Low volume rollout:** low operational cost profile +- **Moderate scale rollout:** moderate profile driven mostly by AI generation invocations +- **High scale rollout:** AI + observability become primary contributors; retry discipline materially affects cost + +### 9.4 Cost Control Requirements +**CO-001** Regeneration must only occur when required by structural changes. + +**CO-002** Retry policy must differentiate transient from permanent failures to prevent waste. + +**CO-003** Event payloads should include only contract-required fields to control transport and storage overhead. + +**CO-004** Retention windows for diagnostics and traces must be policy-driven and periodically reviewed. + +### 9.5 Baseline Monthly Planning Range (for validation, non-binding) +Assumptions: +- 10k to 100k generation cycles per month +- 2-3 events per cycle +- low retry rate under healthy operation + +Planning range: +- **Lower bound:** hundreds of USD/month +- **Mid range:** low thousands USD/month +- **Upper planning bound:** several thousands USD/month at higher volume/observability settings + +> Final budget should be re-estimated after pilot telemetry is collected. + +--- + +## 10. Completion and Validation Checklist + +Use this section to validate delivery against requirements and epic/issue scope. + +### 10.1 Contract Completion +- [ ] V1.2 event set published and approved +- [ ] Canonical identity and definition fields verified across all events +- [ ] Enum governance accepted and documented +- [ ] Payload compatibility rules validated + +### 10.2 Security Completion +- [ ] No secrets in event payloads/logs +- [ ] Sanitized diagnostics policy verified +- [ ] Source and event-type authorization constraints validated +- [ ] Tenant isolation controls validated (including mismatch rejection scenarios) + +### 10.2.1 Architecture Compliance Completion +- [ ] Design review confirms conformance to `.github/DEVELOPMENT_GUIDELINES.md` +- [ ] Layer-boundary checklist completed (controller/application/domain/infrastructure responsibilities) +- [ ] No unapproved architecture deviations remain open + +### 10.3 Reliability Completion +- [ ] Exactly-one-terminal guarantee validated +- [ ] Idempotency behavior validated under replay +- [ ] Retry behavior validated against transient/non-transient classes + +### 10.4 Performance Completion +- [ ] End-to-end latency benchmarked +- [ ] Throughput and error-rate SLOs measured +- [ ] Validation overhead assessed and acceptable + +### 10.5 Cost Completion +- [ ] Pilot telemetry collected for actual event and AI volume +- [ ] Monthly run-rate estimate updated from real data +- [ ] Cost controls verified (regeneration gating, retry discipline, retention policy) + +### 10.6 Cross-Project Completion +- [ ] PurposePath issue mapping is complete and current +- [ ] AI project issue mapping is complete and current +- [ ] Contract version agreement is explicit in both projects +- [ ] No requirement marked done unless both project issue tracks are complete + +--- + +## 11. Shared Delivery Governance + +### 11.1 Governance Requirements +**GD-001** Every requirement in this document must map to both: +- one or more PurposePath project issues +- one or more AI project issues + +**GD-002** Contract versions must remain synchronized across both projects before release. + +**GD-003** Contract-breaking changes require explicit approval in both project tracks. + +**GD-004** Completion status must be evaluated using the stricter state: +- if either project shows incomplete, requirement remains incomplete. + +### 11.2 Change-Control Requirements +**GD-010** Any payload field addition/removal or enum change must be recorded as a versioned contract change request. + +**GD-011** Cross-project traceability tables must be updated in the same review cycle as contract changes. + +**GD-012** Contract examples and requirements text must remain semantically consistent across shared docs. + +--- + +## Appendix A - Requirement-to-Issue Mapping + +| Requirement Area | Primary Issues | +|---|---| +| Contract baseline and semantics | #666, #669, #671 | +| Schema and model alignment | #667, #668 | +| Orchestration behavior and idempotency | #670 | +| Cleanup/deprecation | #672 | +| Validation and rollout | #673 | +| Program-level tracking | #665 | + +--- + +## Appendix B - Requirement IDs Summary + +- Architecture: AR-001..AR-022 +- Contract: CR-001..CR-062 +- Functional: FR-001..FR-031 +- Security: SR-001..SR-021 +- Performance/Reliability: PR-001..PR-021 +- Cost: CO-001..CO-004 + +--- + +## Appendix C - Cross-Project Issue Mapping (Template) + +| Requirement Area | PurposePath Issues | AI Project Issues | +|---|---|---| +| Contract baseline and semantics | #666, #669, #671 | PurposePath_AI #243 | +| Schema and model alignment | #667, #668 | PurposePath_AI #243 | +| Orchestration behavior and idempotency | #670 | PurposePath_AI #243 | +| Cleanup/deprecation | #672 | PurposePath_AI #243 | +| Validation and rollout | #673 | PurposePath_AI #243 | +| Program-level tracking | #665 | PurposePath_AI #243 | + +> Current AI-side tracking anchor is `PurposePath_AI #243`. If the AI team splits work into child issues, replace each row with the specific child issue IDs. diff --git a/docs/shared/Specifications/README.md b/docs/shared/Specifications/README.md index 5e7be1f8..618d4453 100644 --- a/docs/shared/Specifications/README.md +++ b/docs/shared/Specifications/README.md @@ -18,9 +18,10 @@ Specifications/ │ ├── account-api.md # Account, billing, subscriptions (consolidated) │ ├── account-gap.md # Account service gap analysis │ ├── business-foundation-api.md # Business foundation, wizard, values, ICAs -│ ├── people-service.md # People CRUD operations -│ ├── org-structure-service.md # Roles, org chart (user endpoints) +│ ├── org-structure-service.md # People + roles + org chart (user endpoints) │ ├── dashboard-service.md # Dashboard configuration and widgets +│ ├── integration-service/ # Integration user workflows +│ │ └── integration-service.md │ ├── common-patterns.md # Shared patterns & data models │ ├── traction-service/ # Traction feature APIs │ │ ├── README.md # Traction service index @@ -38,6 +39,7 @@ Specifications/ │ ├── admin-portal/ # Admin Portal (Internal) │ └── admin-api-specification.md # Complete admin API spec (v2.0) +│ └── integration-admin-api-specification.md # Integration admin workflows │ ├── ai-user/ # AI/Coaching Services │ └── backend-integration-unified-ai.md # Unified AI/Coaching API @@ -62,9 +64,9 @@ Specifications/ | [Business Foundation Service](./user-app/business-foundation-service.md) | Business setup, wizard, values | ~25 | | [Account API](./user-app/account-api.md) | Auth, billing, subscriptions (consolidated) | ~40 | | [AI/Coaching Service](./user-app/coaching-service.md) | AI/ML coaching features | ~20 | -| [People Service](./user-app/people-service.md) | Person management, tags, types | ~25 | -| [Org Structure Service](./user-app/org-structure-service.md) | Roles, relationships, org chart | ~20 | +| [Org Structure Service](./user-app/org-structure-service.md) | People, roles, relationships, org chart | ~45 | | [Dashboard Service](./user-app/dashboard-service.md) | Dashboard configuration, widgets | ~15 | +| [Integration Service](./user-app/integration-service/integration-service.md) | Connected systems, integrations, testing lifecycle | ~14 | | [Traction Service](./user-app/traction-service/README.md) | Goals, Measures, Actions, Issues | ~66 | **Total User App Endpoints:** ~227 @@ -74,6 +76,7 @@ Specifications/ | Document | Description | Endpoints | |----------|-------------|-----------| | [Admin API v2.0](./admin-portal/admin-api-specification.md) | Complete admin portal spec (updated Feb 4, 2026) | 88 | +| [Integration Admin API](./admin-portal/integration-admin-api-specification.md) | Integration metadata sync and catalog/system/parameter definitions | ~8 | **Total Admin Endpoints:** 88 @@ -111,19 +114,21 @@ Each API document follows this structure: 1. **Header** - Version, dates, base URL 2. **Overview** - Service purpose, key concepts -3. **Endpoints** - Grouped by resource/feature +3. **Change Log** - A table with version, date and brief summary of the change +4. **Endoint List** a list of all endpoints in the document with a hyperlink to the detail section +5. **Endpoints** - Grouped by resource/feature - HTTP method + path - Request/response examples - Field constraints - Error responses -4. **Data Types** - TypeScript interfaces, enums -5. **Error Codes** - Standard error codes +6. **Data Types** - TypeScript interfaces, enums +7. **Error Codes** - Standard error codes ### Naming Conventions | Type | Convention | Example | |------|------------|---------| -| File names | kebab-case | `people-service.md` | +| File names | kebab-case | `org-structure-service.md` | | Endpoint paths | kebab-case | `/api/people/{id}/tags` | | Request/Response fields | snake_case | `first_name`, `created_at` | | TypeScript types | PascalCase | `PersonResponse` | diff --git a/docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md b/docs/shared/Specifications/ai-admin/admin_ai_specifications.md similarity index 96% rename from docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md rename to docs/shared/Specifications/ai-admin/admin_ai_specifications.md index ef5814f1..4bce156a 100644 --- a/docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md +++ b/docs/shared/Specifications/ai-admin/admin_ai_specifications.md @@ -1,1147 +1,1147 @@ -# Admin AI Specifications - LLM Topic Management - -- Last Updated: February 13, 2026 -- Version: 3.1 - -## Revision History - -| Date | Version | Description | -|------|---------|-------------| -| 2026-02-13 | 3.1 | Synced spec to implementation for admin model responses, topic auth, topic type terminology, and conversation extraction config. Updated `/models` response shape (`ApiResponse[LLMModelsResponse]`), enforced admin role on topics routes, standardized `measure_system`, updated `conversation_config.max_turns`, and documented extraction model behavior/defaults. | -| 2026-01-30 | 3.0 | **Issue #158 Completion:** Added tier-based LLM model selection and topic access control. Replaced `model_code` with `basic_model_code` and `premium_model_code`. Added `tier_level` field (FREE, BASIC, PREMIUM, ULTIMATE). | -| 2026-01-25 | 2.0 | **Issue #196 Completion:** Fixed category enum values to match actual TopicCategory implementation, verified all field values match constants.py | -| 2025-12-25 | 1.0 | Initial admin specification | - ---- - -## Overview - -This document specifies all admin endpoints for managing the LLM Topic system. Admin users can update topic configurations, manage prompts, and test topics. - -**Important:** Most topics are defined in the code-based `endpoint_registry`, but admin create/delete endpoints also exist in the API (currently not used by the Admin UI). In practice, admins mainly: -- Update topic configurations (tier level, dual LLM models, temperature, prompts, etc.) -- Manage prompt content (system, user, assistant prompts) -- Test topic configurations before activation - -### Tier-Based Access Control (Issue #158) - -Each topic has a `tier_level` that controls: -1. **Topic Access**: Which subscription tiers can access the topic -2. **Model Selection**: Which LLM model to use based on user's tier - -**Tier Levels:** -- **FREE**: Users can access only FREE topics, uses `basic_model_code` -- **BASIC**: Users can access FREE + BASIC topics, uses `basic_model_code` -- **PREMIUM**: Users can access FREE + BASIC + PREMIUM topics, uses `premium_model_code` -- **ULTIMATE**: Users can access all topics, uses `premium_model_code` - -**Dual Model Configuration:** -- `basic_model_code`: LLM model for FREE and BASIC tier users -- `premium_model_code`: LLM model for PREMIUM and ULTIMATE tier users -- Admins can set different models for each tier (e.g., Claude Haiku for basic, Claude Sonnet for premium) - ---- - -## Implementation Status - -| Endpoint | Status | Notes | -|----------|--------|-------| -| GET /topics | ✅ Implemented | List topics from registry + DB overrides | -| GET /topics/{topic_id} | ✅ Implemented | | -| PUT /topics/{topic_id} | ✅ Implemented | Update topic config | -| GET /topics/{topic_id}/prompts/{prompt_type} | ✅ Implemented | | -| PUT /topics/{topic_id}/prompts/{prompt_type} | ✅ Implemented | | -| POST /topics/{topic_id}/prompts | ✅ Implemented | | -| DELETE /topics/{topic_id}/prompts/{prompt_type} | ✅ Implemented | | -| GET /models | ✅ Implemented | | -| POST /topics/validate | ✅ Implemented | | -| POST /topics/{topic_id}/test | ✅ Implemented | **New** - Test with auto-enrichment | -| GET /topics/stats | ✅ Implemented | Dashboard metrics endpoint used by LLM dashboard | -| GET /topics/{topic_id}/stats | ⏳ Planned | Usage statistics | - ---- - -## Authentication - -All endpoints in this document require a bearer token: - -- **Authentication**: `Authorization: Bearer {token}` must be present and valid -- **Authorization (current implementation)**: - - `/api/v1/admin/models*` endpoints enforce admin access (`ADMIN` or `OWNER`) via `require_admin_access` - - `/api/v1/admin/topics*` endpoints enforce admin access (`ADMIN` or `OWNER`) via `require_admin_access` -- **Headers**: - - `Authorization: Bearer {token}` - - `Content-Type: application/json` - ---- - -## Endpoints - -### 1. List Topics - -**Purpose:** Get all topics (active and inactive) for admin management - -**Endpoint:** - -```http -GET /api/v1/admin/topics -``` - -**Query Parameters:** - -| Parameter | Type | Required | Default | Description | Allowed Values | -|-----------|------|----------|---------|-------------|----------------| -| `page` | integer | No | 1 | Page number | >= 1 | -| `page_size` | integer | No | 50 | Items per page (max 100) | 1-100 | -| `category` | string | No | - | Filter by category | `onboarding`, `conversation`, `insights`, `strategic_planning`, `operations_ai`, `operations_strategic_integration`, `analysis` | -| `topic_type` | string | No | - | Filter by type | `conversation_coaching`, `single_shot`, `measure_system` | -| `is_active` | boolean | No | - | Filter by active status | `true`, `false` | -| `search` | string | No | - | Search in name/description | Max 100 chars | - -**Response:** - -```json -{ - "topics": [ - { - "topic_id": "core_values_coaching", - "topic_name": "Core Values - Coaching Session", - "category": "core_values", - "topic_type": "conversation_coaching", - "tier_level": "free", - "basic_model_code": "CLAUDE_3_5_HAIKU", - "premium_model_code": "CLAUDE_3_5_SONNET_V2", - "temperature": 0.7, - "max_tokens": 2000, - "is_active": true, - "description": "Explore core values through conversation", - "display_order": 1, - "from_database": true, - "templates": [ - {"prompt_type": "system", "is_defined": true}, - {"prompt_type": "user", "is_defined": false} - ], - "created_at": "2024-11-01T10:00:00Z", - "updated_at": "2024-11-13T15:30:00Z", - "created_by": "admin_123" - } - ], - "total": 12, - "page": 1, - "page_size": 50, - "has_more": false -} -``` - -**Response Field Descriptions:** - -| Field | Description | -|-------|-------------| -| `from_database` | `true` = Topic config stored in DB, `false` = Using registry defaults | -| `templates` | Array of allowed templates with `is_defined` indicating if uploaded to S3 | - -**Status Codes:** - -- `200 OK`: Success -- `401 Unauthorized`: Missing or invalid auth token -- `403 Forbidden`: Insufficient permissions - ---- - -### 2. Get Topic Details - -**Purpose:** Get complete details for a specific topic including prompts and allowed parameters - -**Endpoint:** - -```http -GET /api/v1/admin/topics/{topic_id} -``` - -**Path Parameters:** - -| Parameter | Type | Required | Description | Format | -|-----------|------|----------|-------------|--------| -| `topic_id` | string | Yes | Unique topic identifier | snake_case, 3-50 chars | - -**Query Parameters:** - -| Parameter | Type | Required | Default | Description | -|-----------|------|----------|---------|-------------| -| `include_schema` | boolean | No | `false` | Include JSON schema of the response model for template design | - -**Response:** - -```json -{ - "topic_id": "core_values_coaching", - "topic_name": "Core Values - Coaching Session", - "category": "core_values", - "topic_type": "conversation_coaching", - "description": "Explore your core values through conversation", - "tier_level": "free", - "basic_model_code": "CLAUDE_3_5_HAIKU", - "premium_model_code": "CLAUDE_3_5_SONNET_V2", - "temperature": 0.7, - "max_tokens": 2000, - "top_p": 1.0, - "frequency_penalty": 0.0, - "presence_penalty": 0.0, - "is_active": true, - "display_order": 1, - "from_database": true, - "prompts": [ - { - "prompt_type": "system", - "s3_bucket": "purposepath-prompts-prod", - "s3_key": "prompts/core_values_coaching/system.md", - "updated_at": "2024-11-13T15:30:00Z", - "updated_by": "admin_123" - }, - { - "prompt_type": "user", - "s3_bucket": "purposepath-prompts-prod", - "s3_key": "prompts/core_values_coaching/user.md", - "updated_at": "2024-11-13T15:30:00Z", - "updated_by": "admin_123" - } - ], - "template_status": [ - { - "prompt_type": "system", - "is_defined": true, - "s3_bucket": "purposepath-prompts-prod", - "s3_key": "prompts/core_values_coaching/system.md", - "updated_at": "2024-11-13T15:30:00Z", - "updated_by": "admin_123" - }, - { - "prompt_type": "user", - "is_defined": false, - "s3_bucket": null, - "s3_key": null, - "updated_at": null, - "updated_by": null - } - ], - "allowed_parameters": [ - { - "name": "user_name", - "type": "string", - "required": true, - "description": "User's display name" - }, - { - "name": "core_values", - "type": "string", - "required": false, - "description": "User's defined core values (auto-enriched from profile)" - }, - { - "name": "purpose", - "type": "string", - "required": false, - "description": "User's purpose statement (auto-enriched from profile)" - } - ], - "conversation_config": { - "max_messages_to_llm": 30, - "inactivity_timeout_minutes": 30, - "session_ttl_days": 14, - "max_turns": 20, - "extraction_model_code": "CLAUDE_3_5_HAIKU" - }, - "response_schema": null, - "created_at": "2024-11-01T10:00:00Z", - "updated_at": "2024-11-13T15:30:00Z", - "created_by": "admin_123" -} -``` - -**Response Schema (when `include_schema=true`):** - -When the `include_schema` query parameter is set to `true`, the response includes the JSON schema of the expected response model. This is useful for template authors to understand what output fields their prompts should generate. - -Example with `include_schema=true`: - -```json -{ - "topic_id": "niche_review", - "...": "...other fields...", - "response_schema": { - "title": "OnboardingReviewResponse", - "type": "object", - "properties": { - "strengths": { - "type": "array", - "items": {"type": "string"}, - "description": "List of strengths identified" - }, - "weaknesses": { - "type": "array", - "items": {"type": "string"}, - "description": "List of weaknesses or areas for improvement" - }, - "recommendations": { - "type": "array", - "items": {"type": "string"}, - "description": "List of actionable recommendations" - }, - "alignment_score": { - "type": "integer", - "minimum": 0, - "maximum": 100, - "description": "Overall alignment score" - }, - "summary": { - "type": "string", - "description": "Summary of the analysis" - } - }, - "required": ["strengths", "weaknesses", "recommendations", "alignment_score", "summary"] - } -} -``` - -**Note:** The `response_schema` is `null` when: -- `include_schema=false` (default) -- The topic is not in the endpoint registry (custom topics) -- The response model is not registered in the response model registry - -**Conversation Config (conversation_coaching topics only):** - -For topics with `topic_type: "conversation_coaching"`, the response includes `conversation_config`: - -| Field | Type | Range | Default | Description | -|-------|------|-------|---------|-------------| -| `max_messages_to_llm` | integer | 5-100 | 30 | Maximum messages to include in LLM context (sliding window) | -| `inactivity_timeout_minutes` | integer | 5-1440 | 30 | Minutes of inactivity before session auto-pauses | -| `session_ttl_days` | integer | 1-90 | 14 | Days to keep paused/completed sessions before deletion | -| `max_turns` | integer | 0-100 | 0 | Maximum conversation turns (0 means unlimited) | -| `extraction_model_code` | string | - | CLAUDE_3_5_HAIKU | MODEL_REGISTRY code for extraction (e.g., CLAUDE_3_5_HAIKU, CLAUDE_3_5_SONNET_V2) | - -**Compatibility Note:** Existing records may still contain `estimated_messages`. The API maps legacy `estimated_messages` to `max_turns` for backward compatibility. - -### Extraction Model Runtime Behavior - -For `conversation_coaching` completion/extraction: - -- API uses `conversation_config.extraction_model_code` when provided -- Default extraction model is `CLAUDE_3_5_HAIKU` -- If configured extraction model code is not present in `MODEL_REGISTRY`, runtime falls back to `CLAUDE_3_HAIKU` -- Extraction call runs with `temperature=0.3` -- Extraction max tokens are capped to `min(8192, extraction_model.max_tokens)` - -**Template Status:** - -The `template_status` array shows each allowed template and its definition status: - -| Field | Description | -|-------|-------------| -| `is_defined` | `true` if this prompt has been uploaded to S3 | -| `s3_bucket`, `s3_key` | S3 location (null if not defined) | -| `updated_at`, `updated_by` | Last update info (null if not defined) | - -**Status Codes:** - -- `200 OK`: Success -- `404 Not Found`: Topic does not exist -- `401 Unauthorized`: Missing or invalid auth token -- `403 Forbidden`: Insufficient permissions - ---- - -### 3. Create Topic - -**Purpose:** Create a new topic with configuration - -**Endpoint:** - -```http -POST /api/v1/admin/topics -``` - -**Request Body:** - -```json -{ - "topic_id": "purpose_discovery", - "topic_name": "Purpose Discovery Session", - "category": "purpose", - "topic_type": "conversation_coaching", - "description": "Discover your life's purpose through guided conversation", - "tier_level": "free", - "basic_model_code": "CLAUDE_3_5_HAIKU", - "premium_model_code": "CLAUDE_3_5_SONNET_V2", - "temperature": 0.7, - "max_tokens": 2000, - "top_p": 1.0, - "frequency_penalty": 0.0, - "presence_penalty": 0.0, - "is_active": false, - "display_order": 10 -} -``` - -**Notes:** - -- `CreateTopicRequest` does not accept `conversation_config` or `allowed_parameters` -- For `conversation_coaching` topics, configure extraction settings using `PUT /api/v1/admin/topics/{topic_id}` with `conversation_config` -- `allowed_parameters` are derived from endpoint registry and returned by topic detail endpoints - -**Validation Rules:** - -| Field | Rules | Allowed Values / Format | -|-------|-------|------------------------| -| `topic_id` | Required, unique, lowercase, snake_case, 3-50 chars | Regex: `^[a-z][a-z0-9_]*$` | -| `topic_name` | Required, 3-100 chars | Any printable characters | -| `category` | Required | String (not enum-validated in request model) | -| `topic_type` | Required | Enum currently validated by create model: `conversation_coaching`, `single_shot`, `measure_system` | -| `tier_level` | Optional, default `free` | Enum: `free`, `basic`, `premium`, `ultimate` | -| `basic_model_code` | Required, must be valid model code | Use `GET /api/v1/admin/models` values (used for FREE/BASIC tiers) | -| `premium_model_code` | Required, must be valid model code | Use `GET /api/v1/admin/models` values (used for PREMIUM/ULTIMATE tiers) | -| `temperature` | Required, float | 0.0-2.0 | -| `max_tokens` | Required, integer | 1-100000 (model dependent) | -| `top_p` | Optional, float, default 1.0 | 0.0-1.0 | -| `frequency_penalty` | Optional, float, default 0.0 | -2.0 to 2.0 | -| `presence_penalty` | Optional, float, default 0.0 | -2.0 to 2.0 | -| `display_order` | Optional, integer, default 100 | 1-1000 | -| `description` | Optional | Max 500 chars | -| `is_active` | Optional, boolean, default false | `true`, `false` | - -**Supported Model Codes:** - -Model codes are sourced from `MODEL_REGISTRY` and should be retrieved from `GET /api/v1/admin/models`. - -Examples currently in use: -- `CLAUDE_3_5_HAIKU` -- `CLAUDE_3_5_SONNET_V2` -- `CLAUDE_OPUS_4_5` -- `GPT_4O` - -**Category Values in Registry/List Filtering:** - -- `onboarding` -- `conversation` -- `insights` -- `strategic_planning` -- `operations_ai` -- `operations_strategic_integration` -- `analysis` - -**Topic Type Descriptions:** - -- `conversation_coaching`: Interactive conversational coaching sessions (multi-turn) -- `single_shot`: One-shot evaluations, assessments, and analysis -- `measure_system`: KPI/measure system topic type accepted by current create endpoint validator - -**Prompt Types by Topic Type:** - -| Topic Type | Required Prompts | Description | -|------------|-----------------|-------------| -| `conversation_coaching` | `system`, `initiation`, `resume`, `extraction` | System defines coach behavior; initiation starts new sessions; resume continues paused sessions; extraction captures results | -| `single_shot` | `system`, `user` | System defines behavior; user template with parameters | -| `measure_system` | `system`, `user` | System defines KPI/measure calculation behavior; user template for input | - -**Response:** - -```json -{ - "topic_id": "purpose_discovery", - "created_at": "2024-11-13T16:00:00Z", - "message": "Topic created successfully. Upload prompts to activate." -} -``` - -**Status Codes:** - -- `201 Created`: Success -- `400 Bad Request`: Validation error -- `409 Conflict`: Topic ID already exists -- `401 Unauthorized`: Missing or invalid auth token -- `403 Forbidden`: Insufficient permissions - -**Error Response (Validation):** - -```json -{ - "error": "Validation failed", - "validation_errors": [ - { - "field": "topic_id", - "message": "Topic ID must be snake_case", - "code": "INVALID_FORMAT" - } - ] -} -``` - ---- - -### 4. Update Topic - -**Purpose:** Update topic configuration (excluding prompts) - -**Endpoint:** - -```http -PUT /api/v1/admin/topics/{topic_id} -``` - -**Path Parameters:** - -| Parameter | Type | Required | Description | Format | -|-----------|------|----------|-------------|--------| -| `topic_id` | string | Yes | Unique topic identifier | snake_case, 3-50 chars | - -**Request Body:** - -```json -{ - "topic_name": "Core Values - Updated Name", - "description": "Updated description", - "tier_level": "basic", - "basic_model_code": "CLAUDE_3_5_HAIKU", - "premium_model_code": "CLAUDE_3_5_SONNET_V2", - "temperature": 0.5, - "max_tokens": 1500, - "is_active": true, - "display_order": 5, - "conversation_config": { - "max_messages_to_llm": 30, - "inactivity_timeout_minutes": 45, - "session_ttl_days": 14, - "max_turns": 25, - "extraction_model_code": "CLAUDE_3_5_SONNET_V2" - } -} -``` - -**Notes:** - -- Only include fields you want to update -- Cannot update `topic_id` -- Cannot update `category` or `topic_type` (create new topic instead) -- Cannot update `created_at` or `created_by` -- `allowed_parameters` is not part of `UpdateTopicRequest`; allowed parameters are derived from endpoint registry -- `conversation_config` is only applicable for `conversation_coaching` topic types - -**Response:** - -```json -{ - "topic_id": "core_values_coaching", - "updated_at": "2024-11-13T16:15:00Z", - "message": "Topic updated successfully" -} -``` - -**Status Codes:** - -- `200 OK`: Success -- `400 Bad Request`: Validation error -- `404 Not Found`: Topic does not exist -- `401 Unauthorized`: Missing or invalid auth token -- `403 Forbidden`: Insufficient permissions - ---- - -### 5. Delete Topic - -**Purpose:** Soft delete a topic (mark as inactive) - -**Endpoint:** - -```http -DELETE /api/v1/admin/topics/{topic_id} -``` - -**Query Parameters:** - -| Parameter | Type | Required | Default | Description | Allowed Values | -|-----------|------|----------|---------|-------------|----------------| -| `hard_delete` | boolean | No | false | If true, permanently delete (use with caution) | `true`, `false` | - -**Response:** - -```json -{ - "topic_id": "core_values_coaching", - "deleted_at": "2024-11-13T16:20:00Z", - "message": "Topic deactivated successfully" -} -``` - -**Status Codes:** - -- `200 OK`: Success (soft delete) -- `204 No Content`: Success (hard delete) -- `404 Not Found`: Topic does not exist -- `401 Unauthorized`: Missing or invalid auth token -- `403 Forbidden`: Insufficient permissions - ---- - -### 6. Get Prompt Content - -**Purpose:** Retrieve actual prompt content (markdown text) for editing - -**Endpoint:** - -```http -GET /api/v1/admin/topics/{topic_id}/prompts/{prompt_type} -``` - -**Path Parameters:** - -| Parameter | Type | Required | Description | Allowed Values | -|-----------|------|----------|-------------|----------------| -| `topic_id` | string | Yes | Unique topic identifier | snake_case, 3-50 chars | -| `prompt_type` | string | Yes | Type of prompt | Any `PromptType` value. Effective values are constrained by topic registry allowed prompt types | - -**Response:** - -```json -{ - "topic_id": "core_values_coaching", - "prompt_type": "system", - "content": "You are an expert life coach specializing in helping people discover their core values...\n\n## Your Role\n...", - "s3_key": "prompts/core_values_coaching/system.md", - "updated_at": "2024-11-13T15:30:00Z", - "updated_by": "admin_123" -} -``` - -**Status Codes:** - -- `200 OK`: Success -- `404 Not Found`: Prompt not found on topic -- `422 Unprocessable Entity`: Topic not found in DB/registry, or topic exists in registry but no prompts are stored yet -- `401 Unauthorized`: Missing or invalid auth token -- `403 Forbidden`: Insufficient permissions - ---- - -### 7. Update Prompt Content - -**Purpose:** Update prompt markdown content - -**Endpoint:** - -```http -PUT /api/v1/admin/topics/{topic_id}/prompts/{prompt_type} -``` - -**Path Parameters:** - -| Parameter | Type | Required | Description | Allowed Values | -|-----------|------|----------|-------------|----------------| -| `topic_id` | string | Yes | Unique topic identifier | snake_case, 3-50 chars | -| `prompt_type` | string | Yes | Type of prompt | Any `PromptType` value. Effective values are constrained by topic registry allowed prompt types | - -**Request Body:** - -```json -{ - "content": "# Updated System Prompt\n\nYou are an expert life coach...", - "commit_message": "Improved clarity and added examples" -} -``` - -**Validation:** - -- `content`: Required, markdown text, 1-50,000 chars, UTF-8 encoded -- `commit_message`: Optional, for audit trail, max 200 chars - -**Response:** - -```json -{ - "topic_id": "core_values_coaching", - "prompt_type": "system", - "s3_key": "prompts/core_values_coaching/system.md", - "updated_at": "2024-11-13T16:30:00Z", - "version": null, - "message": "Prompt updated successfully" -} -``` - -**Status Codes:** - -- `200 OK`: Success -- `404 Not Found`: Prompt not found on topic -- `422 Unprocessable Entity`: Invalid/disallowed prompt type, or topic not found in DB/registry -- `401 Unauthorized`: Missing or invalid auth token -- `403 Forbidden`: Insufficient permissions - ---- - -### 8. Create/Upload New Prompt - -**Purpose:** Add a new prompt type to a topic - -**Endpoint:** - -```http -POST /api/v1/admin/topics/{topic_id}/prompts -``` - -**Request Body:** - -```json -{ - "prompt_type": "assistant", - "content": "# Assistant Prompt\n\nProvide helpful guidance..." -} -``` - -**Validation:** - -- `prompt_type`: Required, any `PromptType` value. Must be allowed for the specific topic by endpoint registry rules. -- `content`: Required, markdown text, 1-50,000 chars, UTF-8 encoded - -**Response:** - -```json -{ - "topic_id": "core_values_coaching", - "prompt_type": "assistant", - "s3_key": "prompts/core_values_coaching/assistant.md", - "created_at": "2024-11-13T16:35:00Z", - "message": "Prompt created successfully" -} -``` - -**Status Codes:** - -- `201 Created`: Success -- `422 Unprocessable Entity`: Validation error (invalid prompt type or disallowed prompt type/topic mismatch) -- `409 Conflict`: Prompt type already exists -- `422 Unprocessable Entity`: Topic not found in DB/registry -- `401 Unauthorized`: Missing or invalid auth token -- `403 Forbidden`: Insufficient permissions - ---- - -### 9. Delete Prompt - -**Purpose:** Remove a prompt from a topic - -**Endpoint:** - -```http -DELETE /api/v1/admin/topics/{topic_id}/prompts/{prompt_type} -``` - -**Response:** - -```json -{ - "message": "Prompt deleted successfully" -} -``` - -**Status Codes:** - -- `200 OK`: Success -- `404 Not Found`: Prompt not found -- `422 Unprocessable Entity`: Topic not found in database -- `401 Unauthorized`: Missing or invalid auth token -- `403 Forbidden`: Insufficient permissions - ---- - -### 10. List Available Models - -**Purpose:** Get list of supported LLM models for topic configuration - -**Endpoint:** - -```http -GET /api/v1/admin/models -``` - -**Response:** - -```json -{ - "success": true, - "data": { - "models": [ - { - "code": "CLAUDE_3_5_SONNET_V2", - "provider": "bedrock", - "modelName": "anthropic.claude-3-5-sonnet-20241022-v2:0", - "version": "20241022", - "capabilities": ["chat", "analysis", "streaming", "function_calling", "vision"], - "maxTokens": 200000, - "costPer1kTokens": 0.003, - "isActive": true - } - ], - "providers": ["bedrock", "openai"], - "totalCount": 2 - } -} -``` - -**Response Notes:** -- Wrapped in `ApiResponse` (`success`, `data`, optional `error`) -- Field names follow current API aliases (`modelName`, `maxTokens`, `costPer1kTokens`, `isActive`, `totalCount`) -- Pricing is returned as a single `costPer1kTokens` value - -**Status Codes:** - -- `200 OK`: Success -- `401 Unauthorized`: Missing or invalid auth token -- `403 Forbidden`: Insufficient permissions - ---- - -### 11. Validate Topic Configuration - -**Purpose:** Test a topic configuration before saving - -**Endpoint:** - -```http -POST /api/v1/admin/topics/validate -``` - -**Request Body:** - -```json -{ - "topic_id": "test_topic", - "topic_name": "Test Topic", - "category": "analysis", - "topic_type": "single_shot", - "tier_level": "free", - "basic_model_code": "CLAUDE_3_5_HAIKU", - "premium_model_code": "CLAUDE_3_5_SONNET_V2", - "temperature": 0.7, - "max_tokens": 2000, - "prompts": [ - { - "prompt_type": "system", - "content": "Test system prompt with {user_name}" - } - ] -} -``` - -**Response (Valid):** - -```json -{ - "valid": true, - "warnings": [], - "suggestions": [ - "Consider lowering temperature to 0.5 for more consistent responses" - ] -} -``` - -**Response (Invalid):** - -```json -{ - "valid": false, - "errors": [ - { - "field": "prompts[0].content", - "message": "Prompt references parameter 'user_age' which is not defined", - "code": "UNDEFINED_PARAMETER" - } - ], - "warnings": [ - "Temperature 1.2 is high; may produce less consistent results" - ] -} -``` - -**Status Codes:** - -- `200 OK`: Validation complete (check `valid` field) -- `422 Unprocessable Entity`: Malformed request body / schema validation error -- `401 Unauthorized`: Missing or invalid auth token -- `403 Forbidden`: Insufficient permissions - ---- - -### 12. Test Topic - -**Purpose:** Test a topic configuration by executing it with sample parameters. Allows admins to verify prompts work correctly before activating a topic. - -**Endpoint:** - -```http -POST /api/v1/admin/topics/{topic_id}/test -``` - -**Path Parameters:** - -| Parameter | Type | Required | Description | Format | -|-----------|------|----------|-------------|--------| -| `topic_id` | string | Yes | Unique topic identifier | snake_case, 3-50 chars | - -**Request Body:** - -```json -{ - "parameters": { - "website_url": "https://example.com", - "scan_depth": 2 - }, - "allow_inactive": false -} -``` - -**Notes:** - -- **Only supports `single_shot` topics** - conversation_coaching and other topic types cannot be tested via this endpoint -- Parameters not provided will be auto-enriched if a JWT token is supplied (template processor enabled) -- `allow_inactive`: When true, permits testing inactive topics for draft validation - -**Response (Success):** - -```json -{ - "success": true, - "topic_id": "website_scan", - "rendered_system_prompt": "...system prompt after substitutions...", - "rendered_user_prompt": "...user prompt after substitutions...", - "enriched_parameters": { - "website_url": "https://example.com", - "scan_depth": 2, - "website_content": "..." - }, - "response": { - "scan_id": "a1d3b5d8-42cd-4d76-80db-92cf3b4a1a91", - "captured_at": "2025-12-24T05:10:11Z", - "source_url": "https://example.com", - "company_profile": {"company_name": "Acme", "legal_name": "Acme, Inc.", "tagline": "...", "overview": "..."}, - "target_market": {"primary_audience": "...", "segments": ["..."], "pain_points": ["..."]}, - "offers": {"primary_product": "...", "categories": ["..."], "features": ["..."], "differentiators": ["..."]}, - "credibility": {"notable_clients": ["..."], "testimonials": [{"quote": "...", "attribution": "..."}]}, - "conversion": {"primary_cta_text": "...", "primary_cta_url": "https://example.com/demo", "supporting_assets": [{"label": "ROI calculator", "url": "https://example.com/roi"}]} - }, - "response_model": "WebsiteScanResponse", - "response_schema": {"title": "WebsiteScanResponse", "type": "object", "properties": {"scan_id": {"type": "string"}, "captured_at": {"type": "string"}}}, - "llm_metadata": { - "provider": "bedrock", - "model": "anthropic.claude-3-5-sonnet-20241022-v2:0", - "usage": {"prompt_tokens": 1200, "completion_tokens": 600, "total_tokens": 1800}, - "finish_reason": "stop" - }, - "execution_time_ms": 1245.5 -} -``` - -**Response (Error):** - -```json -{ - "success": false, - "topic_id": "website_scan", - "error": "Missing required parameters: website_url", - "execution_time_ms": 150.2 -} -``` - -**Status Codes:** - -- `200 OK`: Test completed (check `success` field) -- `400 Bad Request`: Unsupported topic type (only single_shot topics supported) -- `404 Not Found`: Topic not found -- `422 Unprocessable Entity`: Missing required parameters -- `401 Unauthorized`: Missing or invalid auth token -- `403 Forbidden`: Insufficient permissions -- `500 Internal Server Error`: AI processing or serialization failure - ---- - -### 13. Get Dashboard Topic Stats - -**Purpose:** Retrieve admin dashboard-level LLM metrics (templates, model utilization, interactions summary, system health). - -**Endpoint:** - -```http -GET /api/v1/admin/topics/stats -``` - -**Query Parameters:** - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `start_date` | string (ISO 8601) | No | Filter window start | -| `end_date` | string (ISO 8601) | No | Filter window end | -| `tier` | string | No | Filter by tier | -| `interaction_code` | string | No | Filter by interaction code | -| `model_code` | string | No | Filter by model code | - -**Response:** - -```json -{ - "data": { - "interactions": { - "total": 0, - "by_tier": {}, - "by_model": {}, - "trend": [] - }, - "templates": { - "total": 12, - "active": 10, - "inactive": 2 - }, - "models": { - "total": 15, - "active": 4, - "utilization": { - "CLAUDE_3_5_HAIKU": 8, - "CLAUDE_3_5_SONNET_V2": 6 - } - }, - "system_health": { - "overall_status": "healthy", - "validation_status": "healthy", - "last_validation": "2026-02-13T12:00:00Z", - "critical_issues": [], - "warnings": [], - "recommendations": [], - "service_status": { - "configurations": {"status": "operational", "last_check": "2026-02-13T12:00:00Z", "response_time_ms": 12}, - "templates": {"status": "operational", "last_check": "2026-02-13T12:00:00Z", "response_time_ms": 18}, - "models": {"status": "operational", "last_check": "2026-02-13T12:00:00Z", "response_time_ms": 7} - } - }, - "last_updated": "2026-02-13T12:00:00Z" - } -} -``` - -**Status Codes:** - -- `200 OK`: Success -- `401 Unauthorized`: Missing or invalid auth token -- `403 Forbidden`: Insufficient permissions -- `500 Internal Server Error`: Stats retrieval failed - ---- - -### 14. Get Topic Usage Statistics (Planned) - -**Status:** ⏳ Not yet implemented - -**Purpose:** View usage metrics for a topic - -**Endpoint:** - -```http -GET /api/v1/admin/topics/{topic_id}/stats -``` - -**Query Parameters:** - -| Parameter | Type | Required | Default | Description | -|-----------|------|----------|---------|-------------| -| `start_date` | string | No | 30 days ago | ISO 8601 date | -| `end_date` | string | No | now | ISO 8601 date | - -**Response:** - -```json -{ - "topic_id": "core_values_coaching", - "period": { - "start": "2024-10-14T00:00:00Z", - "end": "2024-11-13T23:59:59Z" - }, - "usage": { - "total_conversations": 1247, - "total_tokens_used": 1850000, - "estimated_cost": 27.75 - } -} -``` - ---- - -## Admin UI Workflows - -### Configuring a Topic - -Topics are defined in the `endpoint_registry` code. Admins configure them by: - -1. **GET** `/api/v1/admin/topics` - View all topics with `templates` (showing allowed and defined prompts) -2. **GET** `/api/v1/admin/topics/{topic_id}` - Get topic details with `template_status` and `allowed_parameters` -3. **POST** `/api/v1/admin/topics/{topic_id}/prompts` - Upload required prompts (system, user, etc.) -4. **PUT** `/api/v1/admin/topics/{topic_id}` - Update model config (temperature, max_tokens, etc.) -5. **POST** `/api/v1/admin/topics/{topic_id}/test` - Test with sample parameters -6. **PUT** `/api/v1/admin/topics/{topic_id}` - Activate topic (`is_active: true`) - -### Editing Prompts - -1. **GET** `/api/v1/admin/topics/{topic_id}` - Get topic details with `template_status` and `allowed_parameters` -2. **GET** `/api/v1/admin/topics/{topic_id}/prompts/{prompt_type}` - Get current content -3. Edit in UI using `allowed_parameters` as available placeholders -4. **PUT** `/api/v1/admin/topics/{topic_id}/prompts/{prompt_type}` - Save changes -5. **POST** `/api/v1/admin/topics/{topic_id}/test` - Test the changes -6. Cache cleared automatically - ---- - -## Error Codes - -Error payloads are returned as FastAPI `HTTPException` details and are endpoint-specific. -Use per-endpoint status code tables above as the source of truth. - ---- - -## Rate Limiting - -Admin endpoints have separate rate limits: - -- **Read operations**: 100 requests/minute -- **Write operations**: 20 requests/minute -- **Bulk operations**: 5 requests/minute - -Headers: - -``` -X-RateLimit-Limit: 100 -X-RateLimit-Remaining: 95 -X-RateLimit-Reset: 1699987200 -``` - ---- - -## Permissions - -Current backend enforcement: - -- `GET/PUT /api/v1/admin/models*`: requires admin role (`ADMIN` or `OWNER`) via `require_admin_access` -- `/api/v1/admin/topics*`: requires admin role (`ADMIN` or `OWNER`) via `require_admin_access` - ---- - -## Versioning - -API Version: `v1` - -Version in URL: `/api/v1/admin/...` - -Breaking changes will increment major version. - ---- - -## Support - -For API issues or questions: - -- Documentation: `llm_topic_architecture.md` -- Frontend changes: `fe_ai_specifications.md` -- Support: backend-team@purposepath.com +# Admin AI Specifications - LLM Topic Management + +- Last Updated: February 13, 2026 +- Version: 3.1 + +## Revision History + +| Date | Version | Description | +|------|---------|-------------| +| 2026-02-13 | 3.1 | Synced spec to implementation for admin model responses, topic auth, topic type terminology, and conversation extraction config. Updated `/models` response shape (`ApiResponse[LLMModelsResponse]`), enforced admin role on topics routes, standardized `measure_system`, updated `conversation_config.max_turns`, and documented extraction model behavior/defaults. | +| 2026-01-30 | 3.0 | **Issue #158 Completion:** Added tier-based LLM model selection and topic access control. Replaced `model_code` with `basic_model_code` and `premium_model_code`. Added `tier_level` field (FREE, BASIC, PREMIUM, ULTIMATE). | +| 2026-01-25 | 2.0 | **Issue #196 Completion:** Fixed category enum values to match actual TopicCategory implementation, verified all field values match constants.py | +| 2025-12-25 | 1.0 | Initial admin specification | + +--- + +## Overview + +This document specifies all admin endpoints for managing the LLM Topic system. Admin users can update topic configurations, manage prompts, and test topics. + +**Important:** Most topics are defined in the code-based `endpoint_registry`, but admin create/delete endpoints also exist in the API (currently not used by the Admin UI). In practice, admins mainly: +- Update topic configurations (tier level, dual LLM models, temperature, prompts, etc.) +- Manage prompt content (system, user, assistant prompts) +- Test topic configurations before activation + +### Tier-Based Access Control (Issue #158) + +Each topic has a `tier_level` that controls: +1. **Topic Access**: Which subscription tiers can access the topic +2. **Model Selection**: Which LLM model to use based on user's tier + +**Tier Levels:** +- **FREE**: Users can access only FREE topics, uses `basic_model_code` +- **BASIC**: Users can access FREE + BASIC topics, uses `basic_model_code` +- **PREMIUM**: Users can access FREE + BASIC + PREMIUM topics, uses `premium_model_code` +- **ULTIMATE**: Users can access all topics, uses `premium_model_code` + +**Dual Model Configuration:** +- `basic_model_code`: LLM model for FREE and BASIC tier users +- `premium_model_code`: LLM model for PREMIUM and ULTIMATE tier users +- Admins can set different models for each tier (e.g., Claude Haiku for basic, Claude Sonnet for premium) + +--- + +## Implementation Status + +| Endpoint | Status | Notes | +|----------|--------|-------| +| GET /topics | ✅ Implemented | List topics from registry + DB overrides | +| GET /topics/{topic_id} | ✅ Implemented | | +| PUT /topics/{topic_id} | ✅ Implemented | Update topic config | +| GET /topics/{topic_id}/prompts/{prompt_type} | ✅ Implemented | | +| PUT /topics/{topic_id}/prompts/{prompt_type} | ✅ Implemented | | +| POST /topics/{topic_id}/prompts | ✅ Implemented | | +| DELETE /topics/{topic_id}/prompts/{prompt_type} | ✅ Implemented | | +| GET /models | ✅ Implemented | | +| POST /topics/validate | ✅ Implemented | | +| POST /topics/{topic_id}/test | ✅ Implemented | **New** - Test with auto-enrichment | +| GET /topics/stats | ✅ Implemented | Dashboard metrics endpoint used by LLM dashboard | +| GET /topics/{topic_id}/stats | ⏳ Planned | Usage statistics | + +--- + +## Authentication + +All endpoints in this document require a bearer token: + +- **Authentication**: `Authorization: Bearer {token}` must be present and valid +- **Authorization (current implementation)**: + - `/api/v1/admin/models*` endpoints enforce admin access (`ADMIN` or `OWNER`) via `require_admin_access` + - `/api/v1/admin/topics*` endpoints enforce admin access (`ADMIN` or `OWNER`) via `require_admin_access` +- **Headers**: + - `Authorization: Bearer {token}` + - `Content-Type: application/json` + +--- + +## Endpoints + +### 1. List Topics + +**Purpose:** Get all topics (active and inactive) for admin management + +**Endpoint:** + +```http +GET /api/v1/admin/topics +``` + +**Query Parameters:** + +| Parameter | Type | Required | Default | Description | Allowed Values | +|-----------|------|----------|---------|-------------|----------------| +| `page` | integer | No | 1 | Page number | >= 1 | +| `page_size` | integer | No | 50 | Items per page (max 100) | 1-100 | +| `category` | string | No | - | Filter by category | `onboarding`, `conversation`, `insights`, `strategic_planning`, `operations_ai`, `operations_strategic_integration`, `analysis` | +| `topic_type` | string | No | - | Filter by type | `conversation_coaching`, `single_shot`, `measure_system` | +| `is_active` | boolean | No | - | Filter by active status | `true`, `false` | +| `search` | string | No | - | Search in name/description | Max 100 chars | + +**Response:** + +```json +{ + "topics": [ + { + "topic_id": "core_values_coaching", + "topic_name": "Core Values - Coaching Session", + "category": "core_values", + "topic_type": "conversation_coaching", + "tier_level": "free", + "basic_model_code": "CLAUDE_3_5_HAIKU", + "premium_model_code": "CLAUDE_3_5_SONNET_V2", + "temperature": 0.7, + "max_tokens": 2000, + "is_active": true, + "description": "Explore core values through conversation", + "display_order": 1, + "from_database": true, + "templates": [ + {"prompt_type": "system", "is_defined": true}, + {"prompt_type": "user", "is_defined": false} + ], + "created_at": "2024-11-01T10:00:00Z", + "updated_at": "2024-11-13T15:30:00Z", + "created_by": "admin_123" + } + ], + "total": 12, + "page": 1, + "page_size": 50, + "has_more": false +} +``` + +**Response Field Descriptions:** + +| Field | Description | +|-------|-------------| +| `from_database` | `true` = Topic config stored in DB, `false` = Using registry defaults | +| `templates` | Array of allowed templates with `is_defined` indicating if uploaded to S3 | + +**Status Codes:** + +- `200 OK`: Success +- `401 Unauthorized`: Missing or invalid auth token +- `403 Forbidden`: Insufficient permissions + +--- + +### 2. Get Topic Details + +**Purpose:** Get complete details for a specific topic including prompts and allowed parameters + +**Endpoint:** + +```http +GET /api/v1/admin/topics/{topic_id} +``` + +**Path Parameters:** + +| Parameter | Type | Required | Description | Format | +|-----------|------|----------|-------------|--------| +| `topic_id` | string | Yes | Unique topic identifier | snake_case, 3-50 chars | + +**Query Parameters:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `include_schema` | boolean | No | `false` | Include JSON schema of the response model for template design | + +**Response:** + +```json +{ + "topic_id": "core_values_coaching", + "topic_name": "Core Values - Coaching Session", + "category": "core_values", + "topic_type": "conversation_coaching", + "description": "Explore your core values through conversation", + "tier_level": "free", + "basic_model_code": "CLAUDE_3_5_HAIKU", + "premium_model_code": "CLAUDE_3_5_SONNET_V2", + "temperature": 0.7, + "max_tokens": 2000, + "top_p": 1.0, + "frequency_penalty": 0.0, + "presence_penalty": 0.0, + "is_active": true, + "display_order": 1, + "from_database": true, + "prompts": [ + { + "prompt_type": "system", + "s3_bucket": "purposepath-prompts-prod", + "s3_key": "prompts/core_values_coaching/system.md", + "updated_at": "2024-11-13T15:30:00Z", + "updated_by": "admin_123" + }, + { + "prompt_type": "user", + "s3_bucket": "purposepath-prompts-prod", + "s3_key": "prompts/core_values_coaching/user.md", + "updated_at": "2024-11-13T15:30:00Z", + "updated_by": "admin_123" + } + ], + "template_status": [ + { + "prompt_type": "system", + "is_defined": true, + "s3_bucket": "purposepath-prompts-prod", + "s3_key": "prompts/core_values_coaching/system.md", + "updated_at": "2024-11-13T15:30:00Z", + "updated_by": "admin_123" + }, + { + "prompt_type": "user", + "is_defined": false, + "s3_bucket": null, + "s3_key": null, + "updated_at": null, + "updated_by": null + } + ], + "allowed_parameters": [ + { + "name": "user_name", + "type": "string", + "required": true, + "description": "User's display name" + }, + { + "name": "core_values", + "type": "string", + "required": false, + "description": "User's defined core values (auto-enriched from profile)" + }, + { + "name": "purpose", + "type": "string", + "required": false, + "description": "User's purpose statement (auto-enriched from profile)" + } + ], + "conversation_config": { + "max_messages_to_llm": 30, + "inactivity_timeout_minutes": 30, + "session_ttl_days": 14, + "max_turns": 20, + "extraction_model_code": "CLAUDE_3_5_HAIKU" + }, + "response_schema": null, + "created_at": "2024-11-01T10:00:00Z", + "updated_at": "2024-11-13T15:30:00Z", + "created_by": "admin_123" +} +``` + +**Response Schema (when `include_schema=true`):** + +When the `include_schema` query parameter is set to `true`, the response includes the JSON schema of the expected response model. This is useful for template authors to understand what output fields their prompts should generate. + +Example with `include_schema=true`: + +```json +{ + "topic_id": "niche_review", + "...": "...other fields...", + "response_schema": { + "title": "OnboardingReviewResponse", + "type": "object", + "properties": { + "strengths": { + "type": "array", + "items": {"type": "string"}, + "description": "List of strengths identified" + }, + "weaknesses": { + "type": "array", + "items": {"type": "string"}, + "description": "List of weaknesses or areas for improvement" + }, + "recommendations": { + "type": "array", + "items": {"type": "string"}, + "description": "List of actionable recommendations" + }, + "alignment_score": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "description": "Overall alignment score" + }, + "summary": { + "type": "string", + "description": "Summary of the analysis" + } + }, + "required": ["strengths", "weaknesses", "recommendations", "alignment_score", "summary"] + } +} +``` + +**Note:** The `response_schema` is `null` when: +- `include_schema=false` (default) +- The topic is not in the endpoint registry (custom topics) +- The response model is not registered in the response model registry + +**Conversation Config (conversation_coaching topics only):** + +For topics with `topic_type: "conversation_coaching"`, the response includes `conversation_config`: + +| Field | Type | Range | Default | Description | +|-------|------|-------|---------|-------------| +| `max_messages_to_llm` | integer | 5-100 | 30 | Maximum messages to include in LLM context (sliding window) | +| `inactivity_timeout_minutes` | integer | 5-1440 | 30 | Minutes of inactivity before session auto-pauses | +| `session_ttl_days` | integer | 1-90 | 14 | Days to keep paused/completed sessions before deletion | +| `max_turns` | integer | 0-100 | 0 | Maximum conversation turns (0 means unlimited) | +| `extraction_model_code` | string | - | CLAUDE_3_5_HAIKU | MODEL_REGISTRY code for extraction (e.g., CLAUDE_3_5_HAIKU, CLAUDE_3_5_SONNET_V2) | + +**Compatibility Note:** Existing records may still contain `estimated_messages`. The API maps legacy `estimated_messages` to `max_turns` for backward compatibility. + +### Extraction Model Runtime Behavior + +For `conversation_coaching` completion/extraction: + +- API uses `conversation_config.extraction_model_code` when provided +- Default extraction model is `CLAUDE_3_5_HAIKU` +- If configured extraction model code is not present in `MODEL_REGISTRY`, runtime falls back to `CLAUDE_3_HAIKU` +- Extraction call runs with `temperature=0.3` +- Extraction max tokens are capped to `min(8192, extraction_model.max_tokens)` + +**Template Status:** + +The `template_status` array shows each allowed template and its definition status: + +| Field | Description | +|-------|-------------| +| `is_defined` | `true` if this prompt has been uploaded to S3 | +| `s3_bucket`, `s3_key` | S3 location (null if not defined) | +| `updated_at`, `updated_by` | Last update info (null if not defined) | + +**Status Codes:** + +- `200 OK`: Success +- `404 Not Found`: Topic does not exist +- `401 Unauthorized`: Missing or invalid auth token +- `403 Forbidden`: Insufficient permissions + +--- + +### 3. Create Topic + +**Purpose:** Create a new topic with configuration + +**Endpoint:** + +```http +POST /api/v1/admin/topics +``` + +**Request Body:** + +```json +{ + "topic_id": "purpose_discovery", + "topic_name": "Purpose Discovery Session", + "category": "purpose", + "topic_type": "conversation_coaching", + "description": "Discover your life's purpose through guided conversation", + "tier_level": "free", + "basic_model_code": "CLAUDE_3_5_HAIKU", + "premium_model_code": "CLAUDE_3_5_SONNET_V2", + "temperature": 0.7, + "max_tokens": 2000, + "top_p": 1.0, + "frequency_penalty": 0.0, + "presence_penalty": 0.0, + "is_active": false, + "display_order": 10 +} +``` + +**Notes:** + +- `CreateTopicRequest` does not accept `conversation_config` or `allowed_parameters` +- For `conversation_coaching` topics, configure extraction settings using `PUT /api/v1/admin/topics/{topic_id}` with `conversation_config` +- `allowed_parameters` are derived from endpoint registry and returned by topic detail endpoints + +**Validation Rules:** + +| Field | Rules | Allowed Values / Format | +|-------|-------|------------------------| +| `topic_id` | Required, unique, lowercase, snake_case, 3-50 chars | Regex: `^[a-z][a-z0-9_]*$` | +| `topic_name` | Required, 3-100 chars | Any printable characters | +| `category` | Required | String (not enum-validated in request model) | +| `topic_type` | Required | Enum currently validated by create model: `conversation_coaching`, `single_shot`, `measure_system` | +| `tier_level` | Optional, default `free` | Enum: `free`, `basic`, `premium`, `ultimate` | +| `basic_model_code` | Required, must be valid model code | Use `GET /api/v1/admin/models` values (used for FREE/BASIC tiers) | +| `premium_model_code` | Required, must be valid model code | Use `GET /api/v1/admin/models` values (used for PREMIUM/ULTIMATE tiers) | +| `temperature` | Required, float | 0.0-2.0 | +| `max_tokens` | Required, integer | 1-100000 (model dependent) | +| `top_p` | Optional, float, default 1.0 | 0.0-1.0 | +| `frequency_penalty` | Optional, float, default 0.0 | -2.0 to 2.0 | +| `presence_penalty` | Optional, float, default 0.0 | -2.0 to 2.0 | +| `display_order` | Optional, integer, default 100 | 1-1000 | +| `description` | Optional | Max 500 chars | +| `is_active` | Optional, boolean, default false | `true`, `false` | + +**Supported Model Codes:** + +Model codes are sourced from `MODEL_REGISTRY` and should be retrieved from `GET /api/v1/admin/models`. + +Examples currently in use: +- `CLAUDE_3_5_HAIKU` +- `CLAUDE_3_5_SONNET_V2` +- `CLAUDE_OPUS_4_5` +- `GPT_4O` + +**Category Values in Registry/List Filtering:** + +- `onboarding` +- `conversation` +- `insights` +- `strategic_planning` +- `operations_ai` +- `operations_strategic_integration` +- `analysis` + +**Topic Type Descriptions:** + +- `conversation_coaching`: Interactive conversational coaching sessions (multi-turn) +- `single_shot`: One-shot evaluations, assessments, and analysis +- `measure_system`: KPI/measure system topic type accepted by current create endpoint validator + +**Prompt Types by Topic Type:** + +| Topic Type | Required Prompts | Description | +|------------|-----------------|-------------| +| `conversation_coaching` | `system`, `initiation`, `resume`, `extraction` | System defines coach behavior; initiation starts new sessions; resume continues paused sessions; extraction captures results | +| `single_shot` | `system`, `user` | System defines behavior; user template with parameters | +| `measure_system` | `system`, `user` | System defines KPI/measure calculation behavior; user template for input | + +**Response:** + +```json +{ + "topic_id": "purpose_discovery", + "created_at": "2024-11-13T16:00:00Z", + "message": "Topic created successfully. Upload prompts to activate." +} +``` + +**Status Codes:** + +- `201 Created`: Success +- `400 Bad Request`: Validation error +- `409 Conflict`: Topic ID already exists +- `401 Unauthorized`: Missing or invalid auth token +- `403 Forbidden`: Insufficient permissions + +**Error Response (Validation):** + +```json +{ + "error": "Validation failed", + "validation_errors": [ + { + "field": "topic_id", + "message": "Topic ID must be snake_case", + "code": "INVALID_FORMAT" + } + ] +} +``` + +--- + +### 4. Update Topic + +**Purpose:** Update topic configuration (excluding prompts) + +**Endpoint:** + +```http +PUT /api/v1/admin/topics/{topic_id} +``` + +**Path Parameters:** + +| Parameter | Type | Required | Description | Format | +|-----------|------|----------|-------------|--------| +| `topic_id` | string | Yes | Unique topic identifier | snake_case, 3-50 chars | + +**Request Body:** + +```json +{ + "topic_name": "Core Values - Updated Name", + "description": "Updated description", + "tier_level": "basic", + "basic_model_code": "CLAUDE_3_5_HAIKU", + "premium_model_code": "CLAUDE_3_5_SONNET_V2", + "temperature": 0.5, + "max_tokens": 1500, + "is_active": true, + "display_order": 5, + "conversation_config": { + "max_messages_to_llm": 30, + "inactivity_timeout_minutes": 45, + "session_ttl_days": 14, + "max_turns": 25, + "extraction_model_code": "CLAUDE_3_5_SONNET_V2" + } +} +``` + +**Notes:** + +- Only include fields you want to update +- Cannot update `topic_id` +- Cannot update `category` or `topic_type` (create new topic instead) +- Cannot update `created_at` or `created_by` +- `allowed_parameters` is not part of `UpdateTopicRequest`; allowed parameters are derived from endpoint registry +- `conversation_config` is only applicable for `conversation_coaching` topic types + +**Response:** + +```json +{ + "topic_id": "core_values_coaching", + "updated_at": "2024-11-13T16:15:00Z", + "message": "Topic updated successfully" +} +``` + +**Status Codes:** + +- `200 OK`: Success +- `400 Bad Request`: Validation error +- `404 Not Found`: Topic does not exist +- `401 Unauthorized`: Missing or invalid auth token +- `403 Forbidden`: Insufficient permissions + +--- + +### 5. Delete Topic + +**Purpose:** Soft delete a topic (mark as inactive) + +**Endpoint:** + +```http +DELETE /api/v1/admin/topics/{topic_id} +``` + +**Query Parameters:** + +| Parameter | Type | Required | Default | Description | Allowed Values | +|-----------|------|----------|---------|-------------|----------------| +| `hard_delete` | boolean | No | false | If true, permanently delete (use with caution) | `true`, `false` | + +**Response:** + +```json +{ + "topic_id": "core_values_coaching", + "deleted_at": "2024-11-13T16:20:00Z", + "message": "Topic deactivated successfully" +} +``` + +**Status Codes:** + +- `200 OK`: Success (soft delete) +- `204 No Content`: Success (hard delete) +- `404 Not Found`: Topic does not exist +- `401 Unauthorized`: Missing or invalid auth token +- `403 Forbidden`: Insufficient permissions + +--- + +### 6. Get Prompt Content + +**Purpose:** Retrieve actual prompt content (markdown text) for editing + +**Endpoint:** + +```http +GET /api/v1/admin/topics/{topic_id}/prompts/{prompt_type} +``` + +**Path Parameters:** + +| Parameter | Type | Required | Description | Allowed Values | +|-----------|------|----------|-------------|----------------| +| `topic_id` | string | Yes | Unique topic identifier | snake_case, 3-50 chars | +| `prompt_type` | string | Yes | Type of prompt | Any `PromptType` value. Effective values are constrained by topic registry allowed prompt types | + +**Response:** + +```json +{ + "topic_id": "core_values_coaching", + "prompt_type": "system", + "content": "You are an expert life coach specializing in helping people discover their core values...\n\n## Your Role\n...", + "s3_key": "prompts/core_values_coaching/system.md", + "updated_at": "2024-11-13T15:30:00Z", + "updated_by": "admin_123" +} +``` + +**Status Codes:** + +- `200 OK`: Success +- `404 Not Found`: Prompt not found on topic +- `422 Unprocessable Entity`: Topic not found in DB/registry, or topic exists in registry but no prompts are stored yet +- `401 Unauthorized`: Missing or invalid auth token +- `403 Forbidden`: Insufficient permissions + +--- + +### 7. Update Prompt Content + +**Purpose:** Update prompt markdown content + +**Endpoint:** + +```http +PUT /api/v1/admin/topics/{topic_id}/prompts/{prompt_type} +``` + +**Path Parameters:** + +| Parameter | Type | Required | Description | Allowed Values | +|-----------|------|----------|-------------|----------------| +| `topic_id` | string | Yes | Unique topic identifier | snake_case, 3-50 chars | +| `prompt_type` | string | Yes | Type of prompt | Any `PromptType` value. Effective values are constrained by topic registry allowed prompt types | + +**Request Body:** + +```json +{ + "content": "# Updated System Prompt\n\nYou are an expert life coach...", + "commit_message": "Improved clarity and added examples" +} +``` + +**Validation:** + +- `content`: Required, markdown text, 1-50,000 chars, UTF-8 encoded +- `commit_message`: Optional, for audit trail, max 200 chars + +**Response:** + +```json +{ + "topic_id": "core_values_coaching", + "prompt_type": "system", + "s3_key": "prompts/core_values_coaching/system.md", + "updated_at": "2024-11-13T16:30:00Z", + "version": null, + "message": "Prompt updated successfully" +} +``` + +**Status Codes:** + +- `200 OK`: Success +- `404 Not Found`: Prompt not found on topic +- `422 Unprocessable Entity`: Invalid/disallowed prompt type, or topic not found in DB/registry +- `401 Unauthorized`: Missing or invalid auth token +- `403 Forbidden`: Insufficient permissions + +--- + +### 8. Create/Upload New Prompt + +**Purpose:** Add a new prompt type to a topic + +**Endpoint:** + +```http +POST /api/v1/admin/topics/{topic_id}/prompts +``` + +**Request Body:** + +```json +{ + "prompt_type": "assistant", + "content": "# Assistant Prompt\n\nProvide helpful guidance..." +} +``` + +**Validation:** + +- `prompt_type`: Required, any `PromptType` value. Must be allowed for the specific topic by endpoint registry rules. +- `content`: Required, markdown text, 1-50,000 chars, UTF-8 encoded + +**Response:** + +```json +{ + "topic_id": "core_values_coaching", + "prompt_type": "assistant", + "s3_key": "prompts/core_values_coaching/assistant.md", + "created_at": "2024-11-13T16:35:00Z", + "message": "Prompt created successfully" +} +``` + +**Status Codes:** + +- `201 Created`: Success +- `422 Unprocessable Entity`: Validation error (invalid prompt type or disallowed prompt type/topic mismatch) +- `409 Conflict`: Prompt type already exists +- `422 Unprocessable Entity`: Topic not found in DB/registry +- `401 Unauthorized`: Missing or invalid auth token +- `403 Forbidden`: Insufficient permissions + +--- + +### 9. Delete Prompt + +**Purpose:** Remove a prompt from a topic + +**Endpoint:** + +```http +DELETE /api/v1/admin/topics/{topic_id}/prompts/{prompt_type} +``` + +**Response:** + +```json +{ + "message": "Prompt deleted successfully" +} +``` + +**Status Codes:** + +- `200 OK`: Success +- `404 Not Found`: Prompt not found +- `422 Unprocessable Entity`: Topic not found in database +- `401 Unauthorized`: Missing or invalid auth token +- `403 Forbidden`: Insufficient permissions + +--- + +### 10. List Available Models + +**Purpose:** Get list of supported LLM models for topic configuration + +**Endpoint:** + +```http +GET /api/v1/admin/models +``` + +**Response:** + +```json +{ + "success": true, + "data": { + "models": [ + { + "code": "CLAUDE_3_5_SONNET_V2", + "provider": "bedrock", + "modelName": "anthropic.claude-3-5-sonnet-20241022-v2:0", + "version": "20241022", + "capabilities": ["chat", "analysis", "streaming", "function_calling", "vision"], + "maxTokens": 200000, + "costPer1kTokens": 0.003, + "isActive": true + } + ], + "providers": ["bedrock", "openai"], + "totalCount": 2 + } +} +``` + +**Response Notes:** +- Wrapped in `ApiResponse` (`success`, `data`, optional `error`) +- Field names follow current API aliases (`modelName`, `maxTokens`, `costPer1kTokens`, `isActive`, `totalCount`) +- Pricing is returned as a single `costPer1kTokens` value + +**Status Codes:** + +- `200 OK`: Success +- `401 Unauthorized`: Missing or invalid auth token +- `403 Forbidden`: Insufficient permissions + +--- + +### 11. Validate Topic Configuration + +**Purpose:** Test a topic configuration before saving + +**Endpoint:** + +```http +POST /api/v1/admin/topics/validate +``` + +**Request Body:** + +```json +{ + "topic_id": "test_topic", + "topic_name": "Test Topic", + "category": "analysis", + "topic_type": "single_shot", + "tier_level": "free", + "basic_model_code": "CLAUDE_3_5_HAIKU", + "premium_model_code": "CLAUDE_3_5_SONNET_V2", + "temperature": 0.7, + "max_tokens": 2000, + "prompts": [ + { + "prompt_type": "system", + "content": "Test system prompt with {user_name}" + } + ] +} +``` + +**Response (Valid):** + +```json +{ + "valid": true, + "warnings": [], + "suggestions": [ + "Consider lowering temperature to 0.5 for more consistent responses" + ] +} +``` + +**Response (Invalid):** + +```json +{ + "valid": false, + "errors": [ + { + "field": "prompts[0].content", + "message": "Prompt references parameter 'user_age' which is not defined", + "code": "UNDEFINED_PARAMETER" + } + ], + "warnings": [ + "Temperature 1.2 is high; may produce less consistent results" + ] +} +``` + +**Status Codes:** + +- `200 OK`: Validation complete (check `valid` field) +- `422 Unprocessable Entity`: Malformed request body / schema validation error +- `401 Unauthorized`: Missing or invalid auth token +- `403 Forbidden`: Insufficient permissions + +--- + +### 12. Test Topic + +**Purpose:** Test a topic configuration by executing it with sample parameters. Allows admins to verify prompts work correctly before activating a topic. + +**Endpoint:** + +```http +POST /api/v1/admin/topics/{topic_id}/test +``` + +**Path Parameters:** + +| Parameter | Type | Required | Description | Format | +|-----------|------|----------|-------------|--------| +| `topic_id` | string | Yes | Unique topic identifier | snake_case, 3-50 chars | + +**Request Body:** + +```json +{ + "parameters": { + "website_url": "https://example.com", + "scan_depth": 2 + }, + "allow_inactive": false +} +``` + +**Notes:** + +- **Only supports `single_shot` topics** - conversation_coaching and other topic types cannot be tested via this endpoint +- Parameters not provided will be auto-enriched if a JWT token is supplied (template processor enabled) +- `allow_inactive`: When true, permits testing inactive topics for draft validation + +**Response (Success):** + +```json +{ + "success": true, + "topic_id": "website_scan", + "rendered_system_prompt": "...system prompt after substitutions...", + "rendered_user_prompt": "...user prompt after substitutions...", + "enriched_parameters": { + "website_url": "https://example.com", + "scan_depth": 2, + "website_content": "..." + }, + "response": { + "scan_id": "a1d3b5d8-42cd-4d76-80db-92cf3b4a1a91", + "captured_at": "2025-12-24T05:10:11Z", + "source_url": "https://example.com", + "company_profile": {"company_name": "Acme", "legal_name": "Acme, Inc.", "tagline": "...", "overview": "..."}, + "target_market": {"primary_audience": "...", "segments": ["..."], "pain_points": ["..."]}, + "offers": {"primary_product": "...", "categories": ["..."], "features": ["..."], "differentiators": ["..."]}, + "credibility": {"notable_clients": ["..."], "testimonials": [{"quote": "...", "attribution": "..."}]}, + "conversion": {"primary_cta_text": "...", "primary_cta_url": "https://example.com/demo", "supporting_assets": [{"label": "ROI calculator", "url": "https://example.com/roi"}]} + }, + "response_model": "WebsiteScanResponse", + "response_schema": {"title": "WebsiteScanResponse", "type": "object", "properties": {"scan_id": {"type": "string"}, "captured_at": {"type": "string"}}}, + "llm_metadata": { + "provider": "bedrock", + "model": "anthropic.claude-3-5-sonnet-20241022-v2:0", + "usage": {"prompt_tokens": 1200, "completion_tokens": 600, "total_tokens": 1800}, + "finish_reason": "stop" + }, + "execution_time_ms": 1245.5 +} +``` + +**Response (Error):** + +```json +{ + "success": false, + "topic_id": "website_scan", + "error": "Missing required parameters: website_url", + "execution_time_ms": 150.2 +} +``` + +**Status Codes:** + +- `200 OK`: Test completed (check `success` field) +- `400 Bad Request`: Unsupported topic type (only single_shot topics supported) +- `404 Not Found`: Topic not found +- `422 Unprocessable Entity`: Missing required parameters +- `401 Unauthorized`: Missing or invalid auth token +- `403 Forbidden`: Insufficient permissions +- `500 Internal Server Error`: AI processing or serialization failure + +--- + +### 13. Get Dashboard Topic Stats + +**Purpose:** Retrieve admin dashboard-level LLM metrics (templates, model utilization, interactions summary, system health). + +**Endpoint:** + +```http +GET /api/v1/admin/topics/stats +``` + +**Query Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `start_date` | string (ISO 8601) | No | Filter window start | +| `end_date` | string (ISO 8601) | No | Filter window end | +| `tier` | string | No | Filter by tier | +| `interaction_code` | string | No | Filter by interaction code | +| `model_code` | string | No | Filter by model code | + +**Response:** + +```json +{ + "data": { + "interactions": { + "total": 0, + "by_tier": {}, + "by_model": {}, + "trend": [] + }, + "templates": { + "total": 12, + "active": 10, + "inactive": 2 + }, + "models": { + "total": 15, + "active": 4, + "utilization": { + "CLAUDE_3_5_HAIKU": 8, + "CLAUDE_3_5_SONNET_V2": 6 + } + }, + "system_health": { + "overall_status": "healthy", + "validation_status": "healthy", + "last_validation": "2026-02-13T12:00:00Z", + "critical_issues": [], + "warnings": [], + "recommendations": [], + "service_status": { + "configurations": {"status": "operational", "last_check": "2026-02-13T12:00:00Z", "response_time_ms": 12}, + "templates": {"status": "operational", "last_check": "2026-02-13T12:00:00Z", "response_time_ms": 18}, + "models": {"status": "operational", "last_check": "2026-02-13T12:00:00Z", "response_time_ms": 7} + } + }, + "last_updated": "2026-02-13T12:00:00Z" + } +} +``` + +**Status Codes:** + +- `200 OK`: Success +- `401 Unauthorized`: Missing or invalid auth token +- `403 Forbidden`: Insufficient permissions +- `500 Internal Server Error`: Stats retrieval failed + +--- + +### 14. Get Topic Usage Statistics (Planned) + +**Status:** ⏳ Not yet implemented + +**Purpose:** View usage metrics for a topic + +**Endpoint:** + +```http +GET /api/v1/admin/topics/{topic_id}/stats +``` + +**Query Parameters:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `start_date` | string | No | 30 days ago | ISO 8601 date | +| `end_date` | string | No | now | ISO 8601 date | + +**Response:** + +```json +{ + "topic_id": "core_values_coaching", + "period": { + "start": "2024-10-14T00:00:00Z", + "end": "2024-11-13T23:59:59Z" + }, + "usage": { + "total_conversations": 1247, + "total_tokens_used": 1850000, + "estimated_cost": 27.75 + } +} +``` + +--- + +## Admin UI Workflows + +### Configuring a Topic + +Topics are defined in the `endpoint_registry` code. Admins configure them by: + +1. **GET** `/api/v1/admin/topics` - View all topics with `templates` (showing allowed and defined prompts) +2. **GET** `/api/v1/admin/topics/{topic_id}` - Get topic details with `template_status` and `allowed_parameters` +3. **POST** `/api/v1/admin/topics/{topic_id}/prompts` - Upload required prompts (system, user, etc.) +4. **PUT** `/api/v1/admin/topics/{topic_id}` - Update model config (temperature, max_tokens, etc.) +5. **POST** `/api/v1/admin/topics/{topic_id}/test` - Test with sample parameters +6. **PUT** `/api/v1/admin/topics/{topic_id}` - Activate topic (`is_active: true`) + +### Editing Prompts + +1. **GET** `/api/v1/admin/topics/{topic_id}` - Get topic details with `template_status` and `allowed_parameters` +2. **GET** `/api/v1/admin/topics/{topic_id}/prompts/{prompt_type}` - Get current content +3. Edit in UI using `allowed_parameters` as available placeholders +4. **PUT** `/api/v1/admin/topics/{topic_id}/prompts/{prompt_type}` - Save changes +5. **POST** `/api/v1/admin/topics/{topic_id}/test` - Test the changes +6. Cache cleared automatically + +--- + +## Error Codes + +Error payloads are returned as FastAPI `HTTPException` details and are endpoint-specific. +Use per-endpoint status code tables above as the source of truth. + +--- + +## Rate Limiting + +Admin endpoints have separate rate limits: + +- **Read operations**: 100 requests/minute +- **Write operations**: 20 requests/minute +- **Bulk operations**: 5 requests/minute + +Headers: + +``` +X-RateLimit-Limit: 100 +X-RateLimit-Remaining: 95 +X-RateLimit-Reset: 1699987200 +``` + +--- + +## Permissions + +Current backend enforcement: + +- `GET/PUT /api/v1/admin/models*`: requires admin role (`ADMIN` or `OWNER`) via `require_admin_access` +- `/api/v1/admin/topics*`: requires admin role (`ADMIN` or `OWNER`) via `require_admin_access` + +--- + +## Versioning + +API Version: `v1` + +Version in URL: `/api/v1/admin/...` + +Breaking changes will increment major version. + +--- + +## Support + +For API issues or questions: + +- Documentation: `llm_topic_architecture.md` +- Frontend changes: `fe_ai_specifications.md` +- Support: backend-team@purposepath.com diff --git a/docs/shared/Specifications/ai-user/backend-integration-unified-ai.md b/docs/shared/Specifications/ai-fe/backend-integration-unified-ai.md similarity index 100% rename from docs/shared/Specifications/ai-user/backend-integration-unified-ai.md rename to docs/shared/Specifications/ai-fe/backend-integration-unified-ai.md diff --git a/docs/shared/Specifications/ai-user/coaching-session-workflow.md b/docs/shared/Specifications/ai-fe/coaching-session-workflow.md similarity index 96% rename from docs/shared/Specifications/ai-user/coaching-session-workflow.md rename to docs/shared/Specifications/ai-fe/coaching-session-workflow.md index 1a32f18a..d8199885 100644 --- a/docs/shared/Specifications/ai-user/coaching-session-workflow.md +++ b/docs/shared/Specifications/ai-fe/coaching-session-workflow.md @@ -1,95 +1,95 @@ -# Coaching Session Workflow Guide - -**Version:** 3.0 -**Last Updated:** February 12, 2026 -**Status:** Active - -[← Back to Backend Integration](./backend-integration-unified-ai.md) - ---- - -## Purpose - -This guide explains the coaching journey in human terms: how a session starts, pauses, resumes, and completes from a user experience perspective. - -**Technical API and payload contracts live in one place only:** -- `docs/shared/Specifications/ai-user/backend-integration-unified-ai.md` -- `docs/shared/Specifications/eventbridge/async-coaching-message-events.md` - ---- - -## Session Lifecycle - -Coaching sessions move through these states: - -- **ACTIVE**: User can continue messaging -- **PAUSED**: User paused or frontend treats an idle session as paused on reopen -- **COMPLETED**: Conversation is done and results are available -- **CANCELLED**: User cancelled the session -- **ABANDONED**: Older session replaced by a fresh start - -The main lifecycle principle: -- **Start means new conversation** -- **Resume means continue existing conversation** - ---- - -## User Journey - -### 1) User opens coaching - -The frontend checks if there is an existing session and whether there is a conflict with another user in the tenant. - -- If no session exists, user starts fresh. -- If a resumable session exists, user chooses to resume or start new. -- If there is a conflict, the UI blocks entry and shows guidance. - -### 2) User sends a message - -Message handling is asynchronous: - -- The API immediately accepts the request and returns a job identifier. -- The backend processes the message in the background. -- Frontend receives the result by WebSocket, or polling fallback. - -This avoids request timeouts on long model responses and keeps the UI responsive. - -### 3) Conversation continues or ends - -Each completed message indicates whether the conversation is final: - -- If not final, the user keeps chatting. -- If final, the UI presents extracted results and closes or transitions the session. - -### 4) User pauses, resumes, or starts over - -- **Pause** explicitly blocks additional messages until resume. -- **Resume** re-enters the existing session context. -- **Start new** creates a fresh session and retires the old one. - ---- - -## Idle and Pause Behavior - -Idle time is treated as a UX signal, not a hard failure for active work: - -- A user who still has an active context can continue conversation flow. -- On re-entry, frontend can present the session as paused and offer resume/new options. - ---- - -## Operational Notes - -- Sessions have TTL-based cleanup. -- Async jobs are temporary and used for status tracking and delivery. -- Frontend should treat job completion events idempotently. - ---- - -## Quick Checklist - -- Frontend checks session status before opening coaching UI -- Frontend handles resume vs start-new explicitly -- Message send uses async job flow -- Completion handling uses finality flag + result payload -- Pause/resume actions are reflected in UI state +# Coaching Session Workflow Guide + +**Version:** 3.0 +**Last Updated:** February 12, 2026 +**Status:** Active + +[← Back to Backend Integration](./backend-integration-unified-ai.md) + +--- + +## Purpose + +This guide explains the coaching journey in human terms: how a session starts, pauses, resumes, and completes from a user experience perspective. + +**Technical API and payload contracts live in one place only:** +- `docs/shared/Specifications/ai-user/backend-integration-unified-ai.md` +- `docs/shared/Specifications/eventbridge/async-coaching-message-events.md` + +--- + +## Session Lifecycle + +Coaching sessions move through these states: + +- **ACTIVE**: User can continue messaging +- **PAUSED**: User paused or frontend treats an idle session as paused on reopen +- **COMPLETED**: Conversation is done and results are available +- **CANCELLED**: User cancelled the session +- **ABANDONED**: Older session replaced by a fresh start + +The main lifecycle principle: +- **Start means new conversation** +- **Resume means continue existing conversation** + +--- + +## User Journey + +### 1) User opens coaching + +The frontend checks if there is an existing session and whether there is a conflict with another user in the tenant. + +- If no session exists, user starts fresh. +- If a resumable session exists, user chooses to resume or start new. +- If there is a conflict, the UI blocks entry and shows guidance. + +### 2) User sends a message + +Message handling is asynchronous: + +- The API immediately accepts the request and returns a job identifier. +- The backend processes the message in the background. +- Frontend receives the result by WebSocket, or polling fallback. + +This avoids request timeouts on long model responses and keeps the UI responsive. + +### 3) Conversation continues or ends + +Each completed message indicates whether the conversation is final: + +- If not final, the user keeps chatting. +- If final, the UI presents extracted results and closes or transitions the session. + +### 4) User pauses, resumes, or starts over + +- **Pause** explicitly blocks additional messages until resume. +- **Resume** re-enters the existing session context. +- **Start new** creates a fresh session and retires the old one. + +--- + +## Idle and Pause Behavior + +Idle time is treated as a UX signal, not a hard failure for active work: + +- A user who still has an active context can continue conversation flow. +- On re-entry, frontend can present the session as paused and offer resume/new options. + +--- + +## Operational Notes + +- Sessions have TTL-based cleanup. +- Async jobs are temporary and used for status tracking and delivery. +- Frontend should treat job completion events idempotently. + +--- + +## Quick Checklist + +- Frontend checks session status before opening coaching UI +- Frontend handles resume vs start-new explicitly +- Message send uses async job flow +- Completion handling uses finality flag + result payload +- Pause/resume actions are reflected in UI state diff --git a/docs/shared/Specifications/ai-fe/measure-integration.md b/docs/shared/Specifications/ai-fe/measure-integration.md new file mode 100644 index 00000000..1cd93369 --- /dev/null +++ b/docs/shared/Specifications/ai-fe/measure-integration.md @@ -0,0 +1,362 @@ +# Measure Integration AI Contract Specification + +**Version:** 1.2 +**Last Updated:** February 17, 2026 +**Status:** Final (Contract Locked) + +--- + +## Purpose + +This document defines the **message contract only** between: +- PurposePath integration backend +- AI SQL-template generation backend + +It specifies event names, payload schemas, enums, and behavioral guarantees. + +--- + +## Scope + +Included: +- EventBridge event contract for SQL template generation +- Required and optional payload fields +- Canonical enums +- Cross-event compatibility rules +- Deterministic/idempotency guarantees + +Excluded (out of scope): +- Internal implementation details of either system +- Storage design, class design, repositories, handlers +- Runtime infrastructure topology +- Deployment details + +--- + +## Event Set + +1. `integration.sql.template.generate.requested` +2. `integration.sql.template.generate.completed` +3. `integration.sql.template.generate.failed` + +--- + +## 1) Common EventBridge Envelope (All Events) + +### Required +- `version` +- `id` +- `detail-type` +- `source` +- `time` +- `detail` (object) + +### Recommended +- `account` +- `region` +- `resources` + +--- + +## 2) Common Detail Identity Block (All Events) + +### Required +- `eventVersion` (string, current: `"1.2"`) +- `provider` (string, current expected value: `"cdata"`) +- `correlationId` (uuid) +- `generationId` (uuid) +- `tenantId` (uuid) +- `measureIntegrationId` (uuid) +- `idempotencyKey` (string) +- `definition.version` (int) +- `definition.hash` (`sha256:*`) + +### Recommended +- `catalogMeasureId` (uuid) +- `measureId` (uuid) + +--- + +## 3) Requested Event Contract + +### Event Identity +- `detail-type = integration.sql.template.generate.requested` +- `source = purposepath.integration` + +### Required `detail` fields +- Common Detail Identity Block +- `system.connectionId` +- `system.cdataConnectionId` +- `intent.templateDefinition` +- `intent.resolvedPeriodWindowStrategy` +- `intent.resolvedContext` (object) +- `parameterModel.allowed` (list) +- `parameterModel.values` (list; may be empty) +- `generationControl.regenerationRequired` (bool) +- `sqlPolicy.dialect` +- `sqlPolicy.bindPlaceholderStyle` (required convention, e.g. `@name`) +- `sqlPolicy.mustUseParameterizedTemplate` (bool) +- `sqlPolicy.forbidden` (list) +- `sqlPolicy.maxRows` (int) +- `sqlPolicy.maxRowsSemantics = FINAL_RESULT_SET_ROWS` + +### Recommended `detail` fields +- `system.workspace` +- `attempt` +- `maxAttempts` +- `requestedAtUtc` +- `timeZone` +- `schemaSnapshotVersion` or `metadataFingerprint` +- `runtimeBindingsExpected` (string[]) + +--- + +## 4) Completed Event Contract + +### Event Identity +- `detail-type = integration.sql.template.generate.completed` +- `source = purposepath.integration.ai` + +### Required `detail` fields +- Common Detail Identity Block +- `status = success` +- `system.connectionId` +- `system.cdataConnectionId` (if available) +- `system.workspace` (required when execution context used workspace) +- `sqlTemplate` (parameterized; no user-value interpolation) +- `sqlTemplateHash` (`sha256:*`) +- `parameterBindingsSchema` (map: name -> type) +- `appliedParameters` (string[]) +- `ignoredParameters` (array of `{ name, reason }`) +- `validated = true` +- `validation.method` +- `validation.attemptCount` +- `generationMetadata.durationMs` +- `generationMetadata.toolCalls` + +### Recommended `detail` fields +- `queryArtifactRef` +- `model` +- `tokensUsed` +- `validationDurationMs` +- `sqlPolicyVersion` + +--- + +## 5) Failed Event Contract + +### Event Identity +- `detail-type = integration.sql.template.generate.failed` +- `source = purposepath.integration.ai` + +### Required `detail` fields +- Common Detail Identity Block +- `status = failed` +- `system.connectionId` +- `system.cdataConnectionId` (if available) +- `system.workspace` (required when execution context used workspace) +- `errorCode` (enum) +- `errorStage` (enum) +- `retryable` (bool) +- `message` (sanitized) +- `durationMs` +- `attemptCount` + +### Optional `detail` fields +- `retryAfterSeconds` (int or null) +- `cdataErrorCode` +- `validationFailures` (array of `{ code, message }`) + +--- + +## 6) Canonical Enums + +### `errorCode` +- `SQL_POLICY_VIOLATION` +- `SQL_VALIDATION_FAILED` +- `SCHEMA_DISCOVERY_FAILED` +- `MCP_TOOL_ERROR` +- `AI_OUTPUT_INVALID` +- `CDATA_AUTH_INVALID` +- `CDATA_CONNECTION_NOT_FOUND` +- `CDATA_RATE_LIMITED` +- `INTERNAL_UNHANDLED` + +### `errorStage` +- `DISCOVER` +- `GENERATE` +- `VALIDATE` +- `REPAIR` +- `PUBLISH` + +### `ignoredParameters[].reason` +- `NOT_NEEDED_FOR_QUERY_SHAPE` +- `UNSUPPORTED_BY_SOURCE_SCHEMA` +- `INVALID_OPERATOR_FOR_PARAMETER` +- `NULL_NOT_ALLOWED` +- `TYPE_MISMATCH` +- `OUT_OF_POLICY` + +### `validationFailures[].code` +- `FORBIDDEN_TOKEN` +- `UNBOUND_PARAMETER` +- `PLACEHOLDER_STYLE_MISMATCH` +- `MAX_ROWS_EXCEEDED` +- `NON_DETERMINISTIC_RESULT_SHAPE` +- `SYNTAX_INVALID` +- `SCHEMA_OBJECT_NOT_FOUND` + +--- + +## 7) Cross-Event Compatibility Rules + +1. `definition.version` and `definition.hash` in terminal events must match the requested event. +2. `runtimeBindingsExpected` (if provided) must be a subset of terminal `parameterBindingsSchema` keys. +3. `sqlPolicy.bindPlaceholderStyle` must match placeholders used in emitted `sqlTemplate`. +4. `appliedParameters` + `ignoredParameters[].name` should represent all provided parameter values considered by the generator. + +--- + +## 8) Behavioral Guarantees + +1. **Idempotency:** at most one terminal event per `generationId`. +2. **Terminal guarantee:** exactly one of completed/failed for a given `generationId`. +3. **Security:** no credentials or secrets in events/logs. +4. **Validation gate:** completed event requires `validated = true`. +5. **Parameterization:** SQL template must not interpolate user values. +6. **Policy semantics:** `maxRows` refers to **final result set rows**, not scanned source rows. + +--- + +## 9) Minimal Payload Skeletons + +### Requested (`detail`) +```json +{ + "eventVersion": "1.2", + "provider": "cdata", + "correlationId": "uuid", + "generationId": "uuid", + "tenantId": "uuid", + "measureIntegrationId": "uuid", + "idempotencyKey": "string", + "definition": { "version": 1, "hash": "sha256:..." }, + "system": { + "connectionId": "uuid", + "cdataConnectionId": "string", + "workspace": "default" + }, + "intent": { + "templateDefinition": "Calculate ... in {TIME_PERIOD} ...", + "resolvedPeriodWindowStrategy": "MONTHLY_CALENDAR", + "resolvedContext": {} + }, + "parameterModel": { + "allowed": [], + "values": [] + }, + "generationControl": { + "regenerationRequired": true + }, + "sqlPolicy": { + "dialect": "cdata-sql", + "bindPlaceholderStyle": "@name", + "mustUseParameterizedTemplate": true, + "forbidden": ["DDL", "DELETE", "UPDATE", "INSERT"], + "maxRows": 1, + "maxRowsSemantics": "FINAL_RESULT_SET_ROWS" + }, + "runtimeBindingsExpected": ["startDate", "endDate"] +} +``` + +### Completed (`detail`) +```json +{ + "eventVersion": "1.2", + "provider": "cdata", + "correlationId": "uuid", + "generationId": "uuid", + "tenantId": "uuid", + "measureIntegrationId": "uuid", + "idempotencyKey": "string", + "definition": { "version": 1, "hash": "sha256:..." }, + "system": { + "connectionId": "uuid", + "cdataConnectionId": "string", + "workspace": "default" + }, + "status": "success", + "sqlTemplate": "SELECT ... @startDate ...", + "sqlTemplateHash": "sha256:...", + "parameterBindingsSchema": { + "startDate": "DateTime", + "endDate": "DateTime" + }, + "appliedParameters": ["customerClass"], + "ignoredParameters": [ + { "name": "currencyCode", "reason": "NOT_NEEDED_FOR_QUERY_SHAPE" } + ], + "validated": true, + "validation": { + "method": "dryRun", + "attemptCount": 1 + }, + "generationMetadata": { + "durationMs": 1234, + "toolCalls": 3 + } +} +``` + +### Failed (`detail`) +```json +{ + "eventVersion": "1.2", + "provider": "cdata", + "correlationId": "uuid", + "generationId": "uuid", + "tenantId": "uuid", + "measureIntegrationId": "uuid", + "idempotencyKey": "string", + "definition": { "version": 1, "hash": "sha256:..." }, + "system": { + "connectionId": "uuid", + "cdataConnectionId": "string", + "workspace": "default" + }, + "status": "failed", + "errorCode": "SQL_VALIDATION_FAILED", + "errorStage": "VALIDATE", + "retryable": false, + "retryAfterSeconds": null, + "message": "sanitized message", + "durationMs": 987, + "attemptCount": 1, + "cdataErrorCode": null, + "validationFailures": [ + { "code": "MAX_ROWS_EXCEEDED", "message": "..." } + ] +} +``` + +--- + +## 10) Contract Authority + +For AI/backend integration, this document is the source of truth for message shape and semantics. + +--- + +## 11) Deprecated Event Mapping (Migration Note) + +The following event names are deprecated and must not be used for new integrations: + +| Deprecated Event | V1.2 Replacement | +|---|---| +| `integration.measure.read.requested` | `integration.sql.template.generate.requested` | +| `integration.measure.read.completed` | `integration.sql.template.generate.completed` | +| `integration.measure.read.failed` | `integration.sql.template.generate.failed` | + +Additional note: +- `integration.measure.read.skipped` has no direct V1.2 equivalent in this SQL-template generation contract and remains out of scope unless explicitly reintroduced in a future contract version. diff --git a/docs/shared/Specifications/admin-portal/admin-api-specification.md b/docs/shared/Specifications/api-admin/admin-api-specification.md similarity index 100% rename from docs/shared/Specifications/admin-portal/admin-api-specification.md rename to docs/shared/Specifications/api-admin/admin-api-specification.md diff --git a/docs/shared/Specifications/api-admin/integration-admin-api-specification.md b/docs/shared/Specifications/api-admin/integration-admin-api-specification.md new file mode 100644 index 00000000..084f8886 --- /dev/null +++ b/docs/shared/Specifications/api-admin/integration-admin-api-specification.md @@ -0,0 +1,365 @@ +# Integration Admin API Specification + +**Service:** Admin Service +**Base Path:** `/admin/api/v1/integrations` +**Version:** 1.1 +**Last Updated:** February 19, 2026 + +## Overview + +This specification defines admin-only integration endpoints for metadata synchronization and integration-definition management. + +### Key Capabilities + +- Synchronize available systems from CData +- Define catalog measure intent for SQL-template generation +- Define supported systems per catalog measure (`SystemMeasureConfig`) +- Define parameter metadata (including lookup metadata) +- Generate/persist generic MCP lookup SQL templates + +### Out of Scope + +- User-facing connection/integration/test endpoints +- Runtime execution endpoints + +--- + +## Authentication + +All endpoints require Admin authorization. + +```http +Authorization: Bearer {admin_access_token} +Content-Type: application/json +``` + +--- + +## Endpoints Summary + +| Method | Endpoint | Description | +|---|---|---| +| POST | `/systems/sync` | Trigger systems metadata synchronization from CData | +| GET | `/systems` | List normalized systems catalog | +| GET | `/systems/sync-status` | Get sync job status/diagnostics | +| POST | `/catalog-measures/{catalogMeasureId}/intent` | Create/update measure intent prompt metadata | +| PUT | `/system-measure-configs` | Upsert supported-system definition for catalog measure | +| GET | `/system-measure-configs/{id}` | Get full system-measure config | +| POST | `/system-measure-configs/{id}/parameters` | Create/update parameter definitions | +| POST | `/system-measure-configs/{id}/parameters/{paramId}/lookup-sql/generate` | Generate/persist generic lookup SQL via MCP | + +--- + +## Common Data Models + +### IntegrationSystemCatalogItem + +```typescript +interface IntegrationSystemCatalogItem { + systemId: string; // UUID (internal) + sourceSystemId: string; // provider-native ID + code: string; // stable key, e.g. 'salesforce' + name: string; + category: string; + iconUrl?: string; + isActive: boolean; + syncedAtUtc: string; // ISO-8601 UTC + createdAtUtc: string; + updatedAtUtc: string; +} +``` + +### SystemMeasureConfigResponse + +```typescript +interface SystemMeasureConfigResponse { + id: string; // UUID + catalogMeasureId: string; // UUID + systemId: string; // UUID + isSupported: boolean; + intentTemplateVersion: number; // >= 1 + updatedAtUtc: string; + updatedBy: string; + parameters: IntegrationParameterDefinition[]; +} +``` + +### IntegrationParameterDefinition + +```typescript +interface IntegrationParameterDefinition { + parameterId: string; // UUID + friendlyName: string; // max 80 chars + sourceColumnName: string; // max 120 chars + dataType: 'string' | 'number' | 'boolean' | 'date' | 'datetime'; + + hasLookup: boolean; + lookupValueKeyColumn?: string; + lookupValueNameColumn?: string; + lookupSqlTemplate?: string; + + isActive: boolean; + sortOrder: number; // >= 0 +} +``` + +### ApiResponse Envelope + +```typescript +interface ApiResponse { + success: boolean; + data: T | null; + message?: string | null; + error?: string | null; + code?: string | null; + details?: unknown; +} +``` + +--- + +## Endpoint Details + +### 1) Synchronize Systems Metadata + +**POST** `/systems/sync` + +Triggers provider metadata synchronization. + +#### Request Body + +```typescript +interface SyncSystemsRequest { + providerCode: 'cdata'; + fullResync?: boolean; // default false +} +``` + +#### Response (202) + +```json +{ + "success": true, + "data": { + "syncRunId": "6d4468d1-89ce-4996-a5ec-d0fcd1e6fa17", + "providerCode": "cdata", + "startedAtUtc": "2026-02-19T09:02:18Z", + "status": "Running" + } +} +``` + +#### Constraints + +- Endpoint is idempotent for repeated trigger attempts within active run window. +- Upsert behavior must avoid duplicate systems. + +--- + +### 2) List Systems Catalog + +**GET** `/systems` + +#### Query Parameters + +```typescript +interface GetSystemsParams { + providerCode?: 'cdata'; + category?: string; + isActive?: boolean; + page?: number; // default 1 + limit?: number; // default 50, max 200 +} +``` + +#### Response (200) + +Returns `IntegrationSystemCatalogItem[]` with optional pagination metadata. + +--- + +### 3) Get Sync Status + +**GET** `/systems/sync-status` + +#### Response (200) + +```json +{ + "success": true, + "data": { + "lastRunId": "6d4468d1-89ce-4996-a5ec-d0fcd1e6fa17", + "status": "Completed", + "startedAtUtc": "2026-02-19T09:02:18Z", + "completedAtUtc": "2026-02-19T09:02:43Z", + "systemsUpserted": 62, + "errorCode": null, + "errorMessage": null + } +} +``` + +--- + +### 4) Upsert Catalog Measure Intent + +**POST** `/catalog-measures/{catalogMeasureId}/intent` + +Creates/updates intent prompt metadata used to generate SQL templates. + +#### Request Body + +```typescript +interface UpsertCatalogMeasureIntentRequest { + intentTemplate: string; // required, max 4000 chars + promptNotes?: string; // max 4000 chars + dataTypeScope: 'snapshot' | 'aggregate' | 'both'; + aggregationAware: boolean; + versionComment?: string; // max 500 chars +} +``` + +#### Response (200) + +```json +{ + "success": true, + "data": { + "catalogMeasureId": "af5d1450-b89a-4a9f-a4ad-f06144bf1a13", + "intentTemplateVersion": 4, + "updatedAtUtc": "2026-02-19T09:10:12Z" + } +} +``` + +--- + +### 5) Upsert System Measure Config + +**PUT** `/system-measure-configs` + +#### Request Body + +```typescript +interface UpsertSystemMeasureConfigRequest { + id?: string; // optional for create + catalogMeasureId: string; // required UUID + systemId: string; // required UUID + isSupported: boolean; +} +``` + +#### Constraints + +- Unique by (`catalogMeasureId`, `systemId`). +- `isSupported=false` marks config inactive for eligibility checks. + +#### Response (200) + +Returns `SystemMeasureConfigResponse`. + +--- + +### 6) Get System Measure Config + +**GET** `/system-measure-configs/{id}` + +Returns configuration + parameters + lookup SQL metadata. + +--- + +### 7) Upsert Parameter Definitions + +**POST** `/system-measure-configs/{id}/parameters` + +#### Request Body + +```typescript +interface UpsertParameterDefinitionsRequest { + parameters: Array<{ + parameterId?: string; + friendlyName: string; // required, max 80 + sourceColumnName: string; // required, max 120 + dataType: 'string' | 'number' | 'boolean' | 'date' | 'datetime'; + hasLookup: boolean; + lookupValueKeyColumn?: string; + lookupValueNameColumn?: string; + isActive?: boolean; + sortOrder?: number; + }>; +} +``` + +#### Validation Rules + +- `friendlyName` and `sourceColumnName` are required. +- if `hasLookup=true`, both `lookupValueKeyColumn` and `lookupValueNameColumn` are required. +- parameter names must be unique per config. + +--- + +### 8) Generate Lookup SQL via MCP + +**POST** `/system-measure-configs/{id}/parameters/{paramId}/lookup-sql/generate` + +Generates reusable lookup SQL template using MCP and persists it. + +#### Request Body + +```typescript +interface GenerateLookupSqlRequest { + providerCode: 'cdata'; + dataDescription: string; // required, max 2000 chars + tenantProviderContextId: string; // required UUID + overwriteExisting?: boolean; // default false +} +``` + +#### MCP Constraints (Critical) + +Lookup SQL must be generic/reusable and generated with inputs including: +- data source +- tenant/subaccount context +- description of required data + +#### Response (200) + +```json +{ + "success": true, + "data": { + "parameterId": "10f5be08-c8c2-476c-ab18-ae7d2d2ecf1e", + "lookupSqlTemplate": "SELECT Id AS value_key, Name AS value_name FROM Accounts ORDER BY Name", + "generatedAtUtc": "2026-02-19T09:16:50Z", + "generationSource": "MCP" + } +} +``` + +--- + +## Error Responses + +### Standard Error Envelope + +```json +{ + "success": false, + "data": null, + "error": "Human-readable message", + "code": "MACHINE_READABLE_CODE", + "details": null +} +``` + +### Common Error Codes + +- `SYNC_FAILED` +- `SYNC_ALREADY_RUNNING` +- `SYSTEM_NOT_FOUND` +- `CATALOG_MEASURE_NOT_FOUND` +- `SYSTEM_MEASURE_CONFIG_NOT_FOUND` +- `UNSUPPORTED_SYSTEM_MEASURE_COMBINATION` +- `INVALID_PARAMETER_DEFINITION` +- `LOOKUP_METADATA_REQUIRED` +- `MCP_LOOKUP_SQL_GENERATION_FAILED` diff --git a/docs/shared/Specifications/user-app/Websocket/REALTIME_HOOKS_USAGE.md b/docs/shared/Specifications/api-fe/Websocket/REALTIME_HOOKS_USAGE.md similarity index 100% rename from docs/shared/Specifications/user-app/Websocket/REALTIME_HOOKS_USAGE.md rename to docs/shared/Specifications/api-fe/Websocket/REALTIME_HOOKS_USAGE.md diff --git a/docs/shared/Specifications/user-app/Websocket/REALTIME_WEBSOCKET_IMPLEMENTATION.md b/docs/shared/Specifications/api-fe/Websocket/REALTIME_WEBSOCKET_IMPLEMENTATION.md similarity index 100% rename from docs/shared/Specifications/user-app/Websocket/REALTIME_WEBSOCKET_IMPLEMENTATION.md rename to docs/shared/Specifications/api-fe/Websocket/REALTIME_WEBSOCKET_IMPLEMENTATION.md diff --git a/docs/shared/Specifications/user-app/Websocket/SSE_TO_WEBSOCKET_MIGRATION.md b/docs/shared/Specifications/api-fe/Websocket/SSE_TO_WEBSOCKET_MIGRATION.md similarity index 100% rename from docs/shared/Specifications/user-app/Websocket/SSE_TO_WEBSOCKET_MIGRATION.md rename to docs/shared/Specifications/api-fe/Websocket/SSE_TO_WEBSOCKET_MIGRATION.md diff --git a/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_EPIC_SUMMARY.md b/docs/shared/Specifications/api-fe/Websocket/WEBSOCKET_EPIC_SUMMARY.md similarity index 100% rename from docs/shared/Specifications/user-app/Websocket/WEBSOCKET_EPIC_SUMMARY.md rename to docs/shared/Specifications/api-fe/Websocket/WEBSOCKET_EPIC_SUMMARY.md diff --git a/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_FRONTEND_INTEGRATION_SPEC.md b/docs/shared/Specifications/api-fe/Websocket/WEBSOCKET_FRONTEND_INTEGRATION_SPEC.md similarity index 100% rename from docs/shared/Specifications/user-app/Websocket/WEBSOCKET_FRONTEND_INTEGRATION_SPEC.md rename to docs/shared/Specifications/api-fe/Websocket/WEBSOCKET_FRONTEND_INTEGRATION_SPEC.md diff --git a/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_IMPLEMENTATION_PROGRESS.md b/docs/shared/Specifications/api-fe/Websocket/WEBSOCKET_IMPLEMENTATION_PROGRESS.md similarity index 100% rename from docs/shared/Specifications/user-app/Websocket/WEBSOCKET_IMPLEMENTATION_PROGRESS.md rename to docs/shared/Specifications/api-fe/Websocket/WEBSOCKET_IMPLEMENTATION_PROGRESS.md diff --git a/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_MIGRATION_ANALYSIS.md b/docs/shared/Specifications/api-fe/Websocket/WEBSOCKET_MIGRATION_ANALYSIS.md similarity index 100% rename from docs/shared/Specifications/user-app/Websocket/WEBSOCKET_MIGRATION_ANALYSIS.md rename to docs/shared/Specifications/api-fe/Websocket/WEBSOCKET_MIGRATION_ANALYSIS.md diff --git a/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_SESSION_RECOVERY.md b/docs/shared/Specifications/api-fe/Websocket/WEBSOCKET_SESSION_RECOVERY.md similarity index 100% rename from docs/shared/Specifications/user-app/Websocket/WEBSOCKET_SESSION_RECOVERY.md rename to docs/shared/Specifications/api-fe/Websocket/WEBSOCKET_SESSION_RECOVERY.md diff --git a/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_UPDATE_EXAMPLE.md b/docs/shared/Specifications/api-fe/Websocket/WEBSOCKET_UPDATE_EXAMPLE.md similarity index 100% rename from docs/shared/Specifications/user-app/Websocket/WEBSOCKET_UPDATE_EXAMPLE.md rename to docs/shared/Specifications/api-fe/Websocket/WEBSOCKET_UPDATE_EXAMPLE.md diff --git a/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_UPDATE_PROTOCOL.md b/docs/shared/Specifications/api-fe/Websocket/WEBSOCKET_UPDATE_PROTOCOL.md similarity index 100% rename from docs/shared/Specifications/user-app/Websocket/WEBSOCKET_UPDATE_PROTOCOL.md rename to docs/shared/Specifications/api-fe/Websocket/WEBSOCKET_UPDATE_PROTOCOL.md diff --git a/docs/shared/Specifications/user-app/account-api.md b/docs/shared/Specifications/api-fe/account-api.md similarity index 100% rename from docs/shared/Specifications/user-app/account-api.md rename to docs/shared/Specifications/api-fe/account-api.md diff --git a/docs/shared/Specifications/user-app/account-gap.md b/docs/shared/Specifications/api-fe/account-gap.md similarity index 100% rename from docs/shared/Specifications/user-app/account-gap.md rename to docs/shared/Specifications/api-fe/account-gap.md diff --git a/docs/shared/Specifications/user-app/business-foundation-api.md b/docs/shared/Specifications/api-fe/business-foundation-api.md similarity index 98% rename from docs/shared/Specifications/user-app/business-foundation-api.md rename to docs/shared/Specifications/api-fe/business-foundation-api.md index 98e31dd1..80075dd1 100644 --- a/docs/shared/Specifications/user-app/business-foundation-api.md +++ b/docs/shared/Specifications/api-fe/business-foundation-api.md @@ -1157,7 +1157,7 @@ These endpoints update individual sections without affecting others. They may fa { "success": true, "data": { - "currentStep": "number (1-6)", + "currentStep": "number (1-8)", "completedSteps": [1, 2, 3], "skippedSteps": [], "isComplete": "boolean", @@ -1167,10 +1167,10 @@ These endpoints update individual sections without affecting others. They may fa ``` **Notes:** -- `currentStep` - Where user is currently (1=profile, 2=identity, 3=products, 4=market, 5=proposition, 6=review) +- `currentStep` - Where user is currently (1=intro, 2=profile, 3=values, 4=identity, 5=products, 6=market, 7=proposition, 8=review) - `completedSteps` - Array of step numbers user has completed - `skippedSteps` - Array of step numbers user has skipped -- `isComplete` - True if all 6 steps completed +- `isComplete` - True if all 8 steps completed **Error Responses:** - 401 Unauthorized @@ -1189,7 +1189,7 @@ These endpoints update individual sections without affecting others. They may fa ```json { - "currentStep": "number (1-6)", + "currentStep": "number (1-8)", "completedSteps": [1, 2, 3], "skippedSteps": [], "isComplete": "boolean" @@ -1199,9 +1199,9 @@ These endpoints update individual sections without affecting others. They may fa **Response:** 200 OK (same structure as GET response) **Validation:** -- `currentStep` - Required, must be 1-6 -- `completedSteps` - Required array of integers 1-6 -- `skippedSteps` - Required array of integers 1-6 +- `currentStep` - Required, must be 1-8 +- `completedSteps` - Required array of integers 1-8 +- `skippedSteps` - Required array of integers 1-8 - `isComplete` - Required boolean **Frontend Handling:** diff --git a/docs/shared/Specifications/user-app/common-patterns.md b/docs/shared/Specifications/api-fe/common-patterns.md similarity index 100% rename from docs/shared/Specifications/user-app/common-patterns.md rename to docs/shared/Specifications/api-fe/common-patterns.md diff --git a/docs/shared/Specifications/user-app/dashboard-service.md b/docs/shared/Specifications/api-fe/dashboard-service.md similarity index 100% rename from docs/shared/Specifications/user-app/dashboard-service.md rename to docs/shared/Specifications/api-fe/dashboard-service.md diff --git a/docs/shared/Specifications/user-app/index.md b/docs/shared/Specifications/api-fe/index.md similarity index 94% rename from docs/shared/Specifications/user-app/index.md rename to docs/shared/Specifications/api-fe/index.md index 8b2ae888..3f4d8c94 100644 --- a/docs/shared/Specifications/user-app/index.md +++ b/docs/shared/Specifications/api-fe/index.md @@ -30,17 +30,15 @@ This specification is split into multiple documents for efficient AI assistant c - Products & Services Inventory - Wizard Progress Tracking -4. **[People Service](./people-service.md)** - - Person CRUD operations +4. **[Org Structure Service](./org-structure-service.md)** + - People endpoints and contracts - Person tags and types - User-person relationships - -5. **[Org Structure Service](./org-structure-service.md)** - Organization roles and permissions - Organization chart and relationships - User org structure endpoints -6. **[AI/Coaching Service](../ai-user/backend-integration-unified-ai.md)** +5. **[AI/Coaching Service](../ai-user/backend-integration-unified-ai.md)** - AI/ML Endpoints (Alignment, Validation, Suggestions) - Business Insights & Metrics - Coaching Conversations @@ -58,7 +56,13 @@ This specification is split into multiple documents for efficient AI assistant c - Dynamic Widget Data Retrieval - Responsive Grid Layouts -9. **[Common Patterns & Data Models](./common-patterns.md)** +9. **[Integration Service](./integration-service/integration-service.md)** ⭐ NEW (v1.0) + - Connected systems management and diagnostics + - Add-system bootstrap and provider account provisioning + - Measure integration add/edit/test/save lifecycle + - Tested-state gating and execution status visibility + +10. **[Common Patterns & Data Models](./common-patterns.md)** - Authentication Headers - Error Handling - Data Models & Enumerations @@ -285,5 +289,6 @@ REACT_APP_FE_BASE_HEADER_LOGIN=false - [Account Service Specs →](./account-service.md) - [AI/Coaching Service Specs →](../ai-user/backend-integration-unified-ai.md) - [Traction Service Specs →](./traction-service/README.md) -- [People & Org Structure →](./people-service.md) +- [Integration Service Specs →](./integration-service/integration-service.md) +- [People & Org Structure →](./org-structure-service.md) - [Common Patterns & Data Models →](./common-patterns.md) diff --git a/docs/shared/Specifications/user-app/org-structure-service.md b/docs/shared/Specifications/api-fe/org-structure-service.md similarity index 68% rename from docs/shared/Specifications/user-app/org-structure-service.md rename to docs/shared/Specifications/api-fe/org-structure-service.md index 3727acdf..39e34536 100644 --- a/docs/shared/Specifications/user-app/org-structure-service.md +++ b/docs/shared/Specifications/api-fe/org-structure-service.md @@ -5,7 +5,7 @@ **Service Base URL:** `{REACT_APP_ACCOUNT_API_URL}` **Default (Localhost):** `http://localhost:8001` -[← People Service](./people-service.md) | [Back to Index](./index.md) +[People + Org consolidated in this document] | [Back to Index](./index.md) > **📋 NOTE:** Admin Template endpoints have been moved to the [Admin Portal API Specification](../admin-portal/admin-api-specification.md#role-templates). @@ -2879,39 +2879,1559 @@ The following real-time events will be supported: ## Complete API Index + ### People Service (Part 1) -| Method | Endpoint | Description | -|--------|----------|-------------| -| POST | `/auth/login` | Login with username/password | -| POST | `/auth/forgot-username` | Request username reminder | -| PUT | `/user/username` | Change username | -| GET | `/people` | List people | -| GET | `/people/assignable` | Get assignable people for dropdowns | -| GET | `/people/{id}` | Get person details | -| POST | `/people` | Create person | -| PUT | `/people/{id}` | Update person | -| DELETE | `/people/{id}` | Delete person | -| POST | `/people/{id}/activate` | Activate person | -| POST | `/people/{id}/deactivate` | Deactivate person | -| POST | `/people/{id}/link-user` | Link person to user | -| POST | `/people/{id}/tags` | Add tags | -| DELETE | `/people/{id}/tags/{tagId}` | Remove tag | -| GET | `/people/{id}/roles` | Get person's roles | -| GET | `/people/{id}/roles/history` | Get role history | -| POST | `/people/{id}/roles` | Assign role | -| PUT | `/people/{id}/roles/{roleId}/primary` | Set primary role | -| DELETE | `/people/{id}/roles/{roleId}` | Unassign role | -| GET | `/person-types` | List person types | -| GET | `/person-types/{id}` | Get person type | -| POST | `/person-types` | Create person type | -| PUT | `/person-types/{id}` | Update person type | -| DELETE | `/person-types/{id}` | Delete person type | -| POST | `/person-types/{id}/activate` | Activate person type | -| GET | `/person-tags` | List tags | -| POST | `/person-tags` | Create tag | -| PUT | `/person-tags/{id}` | Update tag | -| DELETE | `/person-tags/{id}` | Delete tag | +> Migrated from the former people-service.md to consolidate all People contracts in this Org spec. + +# People & Organizational Structure - Backend Integration Specifications (Part 1: People) + +**Version:** 2.0 +**Last Updated:** December 26, 2025 +**Service Base URL:** `{REACT_APP_ACCOUNT_API_URL}` (People & Auth endpoints) +**Default (Localhost):** `http://localhost:8001` (Account) + +[← Back to Index](./index.md) | [Part 2: Organizational Structure →](./org-structure-service.md) + +## Changelog + +| Version | Date | Changes | +|---------|------|----------| +| 2.0 | December 26, 2025 | **BREAKING:** Converted all JSON properties from snake_case to camelCase to match C#/.NET implementation (e.g., `person_id` → `personId`, `first_name` → `firstName`). Query parameters also converted to camelCase. This matches ASP.NET Core default JSON serialization. | +| 1.1 | December 23, 2025 | Migrated People endpoints from Traction service to Account service | +| 1.0 | December 21, 2025 | Initial version | + +--- + +## Overview + +The People module manages all person records within a tenant, including employees, consultants, vendors, and other stakeholders. This module also handles Person Types, Tags, and the User-Person linking that enables work item assignments. + +### Frontend Implementation + +- **Primary Client:** `accountClient` (axios instance) ⚠️ *Changed from tractionClient* +- **Related Files:** + - `src/services/people-service.ts` - Person CRUD operations + - `src/services/person-types-service.ts` - Person type management + - `src/services/person-tags-service.ts` - Tag management + +### Key Concepts + +- **Person**: Any individual related to the business (may or may not have system access) +- **User**: A Person with PurposePath login credentials +- **PersonType**: Category defining relationship to business (Employee, Vendor, etc.) +- **Tag**: Flexible label for grouping/filtering people +- **Assignable**: Flag indicating if a person can receive work assignments + +### Email Verification + +A Person's `isEmailVerified` flag is set to `true` when: +1. They accept a user invitation and create their account (primary flow) +2. They complete email verification during password reset + +There is no explicit `POST /people/{id}/verify-email` endpoint. Email verification is implicitly handled through the user invitation acceptance flow. + +--- + +## Authentication Changes + +### POST /auth/login + +User authentication with username/password. + +**⚠️ BREAKING CHANGE:** Login now uses `username` instead of `email`. + +**Request:** + +```json +{ + "username": "string", + "password": "string" +} +``` + +**Field Constraints:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| username | string | Yes | 3-50 characters | +| password | string | Yes | Min 8 characters | + +**Response:** + +```json +{ + "success": true, + "data": { + "accessToken": "string", + "refreshToken": "string", + "user": { + "userId": "string (GUID)", + "username": "string", + "personId": "string (GUID)", + "tenantId": "string (GUID)", + "avatarUrl": "string?", + "createdAt": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)", + "status": "active | inactive | locked", + "preferences": {} + }, + "person": { + "id": "string (GUID)", + "firstName": "string", + "lastName": "string", + "email": "string?", + "phone": "string?", + "title": "string?" + }, + "tenant": { + "id": "string (GUID)", + "name": "string" + } + } +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Invalid username or password", + "code": "INVALID_CREDENTIALS" +} +``` + +```json +{ + "success": false, + "error": "Account is locked", + "code": "ACCOUNT_LOCKED", + "details": { + "lockedUntil": "2025-12-22T10:30:00Z" + } +} +``` + +**Frontend Handling:** + +- Stores `accessToken` → `localStorage.accessToken` +- Stores `refreshToken` → `localStorage.refreshToken` +- Stores `tenant.id` → `localStorage.tenantId` +- Person data available for profile display + +--- + +### POST /auth/forgot-username + +Request username reminder via email. + +**Request:** + +```json +{ + "email": "string" +} +``` + +**Field Constraints:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| email | string | Yes | Valid email format | + +**Response (Always 200 to prevent email enumeration):** + +```json +{ + "success": true, + "message": "If the email is associated with any accounts, instructions have been sent." +} +``` + +**Backend Behavior:** + +1. Find all Persons with verified email matching input +2. For each Person linked to a User, include the username +3. Send email listing all associated usernames and tenant names +4. Always return success (even if email not found) + +--- + +### PUT /user/username + +Change current user's username. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Request:** + +```json +{ + "newUsername": "string", + "currentPassword": "string" +} +``` + +**Field Constraints:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| newUsername | string | Yes | 3-50 chars, alphanumeric + `.` `_` `-` `@`, must start with alphanumeric | +| currentPassword | string | Yes | Current password for verification | + +**Response:** + +```json +{ + "success": true, + "data": { + "username": "string", + "previousUsername": "string", + "nextChangeAllowedAt": "string (ISO 8601)" + } +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Username is already taken", + "code": "DUPLICATE_RESOURCE", + "details": { + "field": "newUsername" + } +} +``` + +```json +{ + "success": false, + "error": "Username can only be changed once every 30 days", + "code": "RATE_LIMIT_EXCEEDED", + "details": { + "nextChangeAllowedAt": "2025-01-15T10:30:00Z" + } +} +``` + +```json +{ + "success": false, + "error": "Invalid current password", + "code": "INVALID_CREDENTIALS" +} +``` + +**Business Rules:** + +- Username must be globally unique (case-insensitive) +- Maximum one change per 30 days +- Previous username reserved for 90 days (cannot be claimed by others) +- Email notification sent to linked Person's email + +--- + +## People Endpoints + +### GET /people + +List people with filtering and pagination. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| status | string | `active` | Filter: `active`, `inactive`, `all` | +| isAssignable | boolean | - | Filter by assignable flag | +| personTypeId | string (GUID) | - | Filter by person type | +| tags | string | - | Comma-separated tag GUIDs (any match) | +| search | string | - | Search by name, email, or title | +| page | number | 1 | Page number (1-based) | +| pageSize | number | 20 | Items per page (max 100) | +| sortBy | string | `name` | Sort field: `name`, `createdAt`, `type` | +| sortOrder | string | `asc` | Sort order: `asc`, `desc` | + +**Response:** + +```json +{ + "success": true, + "data": { + "items": [ + { + "id": "string (GUID)", + "firstName": "string", + "lastName": "string", + "email": "string?", + "isEmailVerified": "boolean", + "phone": "string?", + "title": "string?", + "personType": { + "id": "string (GUID)", + "code": "string", + "name": "string" + }, + "isActive": "boolean", + "isAssignable": "boolean", + "primaryRole": { + "id": "string (GUID)", + "name": "string" + }, + "tags": [ + { + "id": "string (GUID)", + "name": "string" + } + ], + "hasSystemAccess": "boolean", + "createdAt": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)?" + } + ], + "pagination": { + "page": "number", + "pageSize": "number", + "totalItems": "number", + "totalPages": "number" + } + } +} +``` + +--- + +### GET /people/assignable + +Get list of people available for work assignment dropdowns. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| tags | string | - | Comma-separated tag GUIDs (any match) | +| personTypeId | string (GUID) | - | Filter by person type | +| search | string | - | Search by name | + +**Response:** + +```json +{ + "success": true, + "data": [ + { + "id": "string (GUID)", + "name": "string", + "title": "string?", + "primaryRole": "string?", + "isCurrentUser": "boolean" + } + ] +} +``` + +**Notes:** + +- Returns only people where `isActive = true` AND `isAssignable = true` +- `isCurrentUser` is `true` if this Person is linked to the authenticated User +- Sorted alphabetically by name, with current user first + +--- + +### GET /people/{id} + +Get detailed person information. + +**Path Parameters:** + +- `id` - Person ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "firstName": "string", + "lastName": "string", + "email": "string?", + "isEmailVerified": "boolean", + "phone": "string?", + "title": "string?", + "personType": { + "id": "string (GUID)", + "code": "string", + "name": "string" + }, + "isActive": "boolean", + "isAssignable": "boolean", + "notes": "string?", + "tags": [ + { + "id": "string (GUID)", + "name": "string" + } + ], + "roles": [ + { + "id": "string (GUID)", + "roleId": "string (GUID)", + "roleCode": "string", + "roleName": "string", + "isPrimary": "boolean", + "effectiveDate": "string (ISO 8601)", + "terminationDate": "string (ISO 8601)?" + } + ], + "linkedUserId": "string (GUID)?", + "hasSystemAccess": "boolean", + "createdAt": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)?", + "createdBy": "string (GUID)", + "updatedBy": "string (GUID)?" + } +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Person not found", + "code": "RESOURCE_NOT_FOUND", + "details": { + "resourceType": "Person", + "resourceId": "guid" + } +} +``` + +--- + +### POST /people + +Create a new person. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Request:** + +```json +{ + "firstName": "string", + "lastName": "string", + "email": "string?", + "phone": "string?", + "title": "string?", + "personTypeId": "string (GUID)", + "isAssignable": "boolean?", + "notes": "string?", + "tags": ["string (GUID)"] +} +``` + +**Field Constraints:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| firstName | string | Yes | 1-100 characters | +| lastName | string | Yes | 1-100 characters | +| email | string | No* | Valid email format, unique within tenant | +| phone | string | No | Max 20 characters | +| title | string | No | Max 100 characters | +| personTypeId | GUID | Yes | Must exist and be active | +| isAssignable | boolean | No | Defaults from PersonType.isAssignableByDefault | +| notes | string | No | Max 2000 characters | +| tags | GUID[] | No | Must be valid tag IDs | + +*Email is required if person will be linked to a User. + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "firstName": "string", + "lastName": "string", + "email": "string?", + "isEmailVerified": false, + "phone": "string?", + "title": "string?", + "personType": { + "id": "string (GUID)", + "code": "string", + "name": "string" + }, + "isActive": true, + "isAssignable": "boolean", + "notes": "string?", + "tags": [], + "roles": [], + "linkedUserId": null, + "hasSystemAccess": false, + "createdAt": "string (ISO 8601)", + "createdBy": "string (GUID)" + } +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Email is already in use within this tenant", + "code": "DUPLICATE_RESOURCE", + "details": { + "field": "email" + } +} +``` + +```json +{ + "success": false, + "error": "Person type not found or inactive", + "code": "VALIDATION_ERROR", + "details": { + "field": "personTypeId" + } +} +``` + +--- + +### PUT /people/{id} + +Update an existing person. + +**Path Parameters:** + +- `id` - Person ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Request:** + +```json +{ + "firstName": "string?", + "lastName": "string?", + "email": "string?", + "phone": "string?", + "title": "string?", + "personTypeId": "string (GUID)?", + "isAssignable": "boolean?", + "notes": "string?" +} +``` + +**Notes:** + +- All fields are optional (partial update) +- Cannot update `isActive` via this endpoint (use activate/deactivate) +- Tags managed via separate endpoints + +**Response:** + +```json +{ + "success": true, + "data": "PersonResponse (full object)" +} +``` + +--- + +### DELETE /people/{id} + +Soft delete (deactivate) a person. Optionally reassign their work items to another person. + +**Path Parameters:** + +- `id` - Person ID (GUID) + +**Query Parameters:** + +- `reassignTo` - Optional: Person ID (GUID) to reassign work items to + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "message": "Person deactivated successfully", + "data": { + "id": "string (GUID)", + "firstName": "string", + "lastName": "string", + "displayName": "string", + "email": "string?", + "phone": "string?", + "title": "string?", + "personType": { + "id": "string (GUID)", + "name": "string" + }, + "isAssignable": false, + "isPrimary": false, + "notes": "string?", + "status": "inactive", + "linkedUserId": "string (GUID)?", + "isEmailVerified": false, + "tags": ["string (GUID)"], + "roleAssignments": [], + "createdBy": "string (GUID)", + "createdAt": "string (ISO 8601)", + "updatedBy": "string (GUID)", + "updatedAt": "string (ISO 8601)" + } +} +``` + +**Error Responses:** + +- **404 Not Found** - Person not found +- **400 Bad Request** - Invalid person ID format, invalid reassignTo ID, or deactivation failed +- **500 Internal Server Error** - Server error + +--- + +### POST /people/{id}/deactivate + +Deactivate a person (compatibility route). Same behavior and payload as `DELETE /people/{id}`. + +**Path Parameters:** + +- `id` - Person ID (GUID) + +**Query Parameters:** + +- `reassignTo` - Optional: Person ID (GUID) to reassign work items to + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "message": "Person deactivated successfully", + "data": { + "id": "string (GUID)", + "firstName": "string", + "lastName": "string", + "displayName": "string", + "email": "string?", + "phone": "string?", + "title": "string?", + "personType": { + "id": "string (GUID)", + "name": "string" + }, + "isAssignable": false, + "isPrimary": false, + "notes": "string?", + "status": "inactive", + "linkedUserId": "string (GUID)?", + "isEmailVerified": false, + "tags": ["string (GUID)"], + "roleAssignments": [], + "createdBy": "string (GUID)", + "createdAt": "string (ISO 8601)", + "updatedBy": "string (GUID)", + "updatedAt": "string (ISO 8601)" + } +} +``` + +**Error Responses:** + +- **404 Not Found** - Person not found +- **400 Bad Request** - Invalid person ID format, invalid reassignTo ID, or deactivation failed +- **500 Internal Server Error** - Server error + +--- + +### POST /people/{id}/activate + +Reactivate a deactivated person. + +**Path Parameters:** + +- `id` - Person ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "message": "Person activated successfully", + "data": { + "id": "string (GUID)", + "firstName": "string", + "lastName": "string", + "displayName": "string", + "email": "string?", + "phone": "string?", + "title": "string?", + "personType": { + "id": "string (GUID)", + "name": "string" + }, + "isAssignable": true, + "isPrimary": false, + "notes": "string?", + "status": "active", + "linkedUserId": "string (GUID)?", + "isEmailVerified": false, + "tags": ["string (GUID)"], + "roleAssignments": [], + "createdBy": "string (GUID)", + "createdAt": "string (ISO 8601)", + "updatedBy": "string (GUID)", + "updatedAt": "string (ISO 8601)" + } +} +``` + +**Error Responses:** + +- **404 Not Found** - Person not found +- **400 Bad Request** - Invalid person ID format or activation failed +- **500 Internal Server Error** - Server error + +--- + +### POST /people/{id}/link-user + +Link a person to an existing user account. + +**Path Parameters:** + +- `id` - Person ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Request:** + +```json +{ + "userId": "string (GUID)" +} +``` + +**Response:** + +```json +{ + "success": true, + "data": { + "personId": "string (GUID)", + "userId": "string (GUID)", + "linkedAt": "string (ISO 8601)" + } +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Person is already linked to a user", + "code": "BUSINESS_RULE_VIOLATION", + "details": { + "existingUserId": "guid" + } +} +``` + +```json +{ + "success": false, + "error": "User is already linked to another person", + "code": "BUSINESS_RULE_VIOLATION", + "details": { + "existingPersonId": "guid" + } +} +``` + +```json +{ + "success": false, + "error": "Person must have a verified email to be linked to a user", + "code": "VALIDATION_ERROR" +} +``` + +--- + +### POST /people/{id}/tags + +Add tags to a person. + +**Path Parameters:** + +- `id` - Person ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Request:** + +```json +{ + "tagIds": ["string (GUID)"] +} +``` + +**Response:** + +```json +{ + "success": true, + "data": { + "tags": [ + { + "id": "string (GUID)", + "name": "string", + "assignedAt": "string (ISO 8601)" + } + ] + } +} +``` + +--- + +### DELETE /people/{id}/tags/{tagId} + +Remove a tag from a person. + +**Path Parameters:** + +- `id` - Person ID (GUID) +- `tagId` - Tag ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true +} +``` + +--- + +## Person Roles Endpoints + +### GET /people/{id}/roles + +Get person's current role assignments. + +**Path Parameters:** + +- `id` - Person ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "data": [ + { + "id": "string (GUID)", + "role": { + "id": "string (GUID)", + "code": "string", + "name": "string" + }, + "isPrimary": "boolean", + "effectiveDate": "string (ISO 8601)", + "terminationDate": null + } + ] +} +``` + +--- + +### GET /people/{id}/roles/history + +Get person's complete role assignment history. + +**Path Parameters:** + +- `id` - Person ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "data": { + "current": [ + { + "id": "string (GUID)", + "role": { + "id": "string (GUID)", + "code": "string", + "name": "string" + }, + "isPrimary": "boolean", + "effectiveDate": "string (ISO 8601)" + } + ], + "historical": [ + { + "id": "string (GUID)", + "role": { + "id": "string (GUID)", + "code": "string", + "name": "string" + }, + "isPrimary": "boolean", + "effectiveDate": "string (ISO 8601)", + "terminationDate": "string (ISO 8601)" + } + ] + } +} +``` + +--- + +### POST /people/{id}/roles + +Assign a role to a person. + +**Path Parameters:** + +- `id` - Person ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Request:** + +```json +{ + "roleId": "string (GUID)", + "isPrimary": "boolean?", + "effectiveDate": "string (ISO 8601)?" +} +``` + +**Field Constraints:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| roleId | GUID | Yes | Must be active role | +| isPrimary | boolean | No | Default: `true` if first role, else `false` | +| effectiveDate | ISO 8601 | No | Default: current date | + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "role": { + "id": "string (GUID)", + "code": "string", + "name": "string" + }, + "isPrimary": "boolean", + "effectiveDate": "string (ISO 8601)", + "previousOccupantTerminated": "boolean" + } +} +``` + +**Notes:** + +- If role already has an occupant, their assignment is auto-terminated +- `previousOccupantTerminated` indicates if this happened + +**Error Responses:** + +```json +{ + "success": false, + "error": "Person is not active", + "code": "BUSINESS_RULE_VIOLATION" +} +``` + +```json +{ + "success": false, + "error": "Role not found or inactive", + "code": "VALIDATION_ERROR", + "details": { + "field": "roleId" + } +} +``` + +--- + +### PUT /people/{id}/roles/{roleId}/primary + +Set a role as the person's primary role. + +**Path Parameters:** + +- `id` - Person ID (GUID) +- `roleId` - Role ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "data": { + "previousPrimaryRoleId": "string (GUID)?", + "newPrimaryRoleId": "string (GUID)" + } +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Person is not assigned to this role", + "code": "VALIDATION_ERROR" +} +``` + +--- + +### DELETE /people/{id}/roles/{roleId} + +Unassign a role from a person. + +**Path Parameters:** + +- `id` - Person ID (GUID) +- `roleId` - Role ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| terminationDate | ISO 8601 | now | When assignment ends | + +**Response:** + +```json +{ + "success": true, + "data": { + "terminated": true, + "terminationDate": "string (ISO 8601)", + "wasPrimary": "boolean", + "newPrimaryRoleId": "string (GUID)?" + } +} +``` + +**Notes:** + +- If this was the primary role and person has other roles, frontend should prompt to select new primary + +--- + +## Person Types Endpoints + +### GET /person-types + +List all person types for the tenant. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| includeInactive | boolean | false | Include inactive types | + +**Response:** + +```json +{ + "success": true, + "data": [ + { + "id": "string (GUID)", + "code": "string", + "name": "string", + "description": "string?", + "isAssignableByDefault": "boolean", + "displayOrder": "number", + "isActive": "boolean", + "createdAt": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)?" + } + ] +} +``` + +--- + +### GET /person-types/{id} + +Get person type details. + +**Path Parameters:** + +- `id` - PersonType ID (GUID) + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "code": "string", + "name": "string", + "description": "string?", + "isAssignableByDefault": "boolean", + "displayOrder": "number", + "isActive": "boolean", + "personCount": "number", + "createdAt": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)?" + } +} +``` + +--- + +### POST /person-types + +Create a new person type. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Request:** + +```json +{ + "code": "string", + "name": "string", + "description": "string?", + "isAssignableByDefault": "boolean", + "displayOrder": "number?" +} +``` + +**Field Constraints:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| code | string | Yes | 2-20 chars, uppercase alphanumeric + underscore, unique within tenant | +| name | string | Yes | 1-100 characters | +| description | string | No | Max 500 characters | +| isAssignableByDefault | boolean | Yes | - | +| displayOrder | number | No | Default: next available | + +**Response:** + +```json +{ + "success": true, + "data": "PersonTypeResponse" +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Person type code already exists", + "code": "DUPLICATE_RESOURCE", + "details": { + "field": "code" + } +} +``` + +--- + +### PUT /person-types/{id} + +Update a person type. + +**Path Parameters:** + +- `id` - PersonType ID (GUID) + +**Request:** + +```json +{ + "name": "string?", + "description": "string?", + "isAssignableByDefault": "boolean?", + "displayOrder": "number?" +} +``` + +**Notes:** + +- `code` cannot be changed after creation +- Changes to `isAssignableByDefault` do not affect existing persons + +**Response:** + +```json +{ + "success": true, + "data": "PersonTypeResponse" +} +``` + +--- + +### DELETE /person-types/{id} + +Deactivate a person type. + +**Path Parameters:** + +- `id` - PersonType ID (GUID) + +**Response:** + +```json +{ + "success": true +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Cannot delete person type with existing persons", + "code": "BUSINESS_RULE_VIOLATION", + "details": { + "personCount": 15 + } +} +``` + +--- + +## Person Tags Endpoints + +### GET /person-tags + +List all tags for the tenant. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "data": [ + { + "id": "string (GUID)", + "name": "string", + "personCount": "number", + "createdAt": "string (ISO 8601)" + +--- + +### POST /person-types/{id}/activate + +Reactivate a deactivated person type. + +**Path Parameters:** + +- `id` - PersonType ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "code": "string", + "name": "string", + "description": "string?", + "isAssignableByDefault": "boolean", + "displayOrder": "number", + "isActive": "boolean", + "createdAt": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)?" + } +} +``` + +**Error Responses:** + +- **404 Not Found** - Person type not found +- **400 Bad Request** - Invalid person type ID format or activation failed +- **500 Internal Server Error** - Server error + } + ] +} +``` + +--- + +### POST /person-tags + +Create a new tag. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Request:** + +```json +{ + "name": "string" +} +``` + +**Field Constraints:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| name | string | Yes | 1-50 characters, unique within tenant | + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "name": "string", + "createdAt": "string (ISO 8601)" + } +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Tag name already exists", + "code": "DUPLICATE_RESOURCE", + "details": { + "field": "name" + } +} +``` + +--- + +### PUT /person-tags/{id} + +Update a tag name. + +**Path Parameters:** + +- `id` - Tag ID (GUID) + +**Request:** + +```json +{ + "name": "string" +} +``` + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "name": "string" + } +} +``` + +--- + +### DELETE /person-tags/{id} + +Delete a tag (cascade removes all person-tag assignments). + +**Path Parameters:** + +- `id` - Tag ID (GUID) + +**Response:** + +```json +{ + "success": true, + "data": { + "assignmentsRemoved": "number" + } +} +``` + +--- + +## Error Codes Reference + +| Code | HTTP Status | Description | +|------|-------------|-------------| +| `VALIDATION_ERROR` | 400 | Input validation failed | +| `INVALID_CREDENTIALS` | 401 | Wrong username/password | +| `UNAUTHORIZED` | 401 | Missing/invalid token | +| `FORBIDDEN` | 403 | Insufficient permissions | +| `RESOURCE_NOT_FOUND` | 404 | Resource does not exist | +| `DUPLICATE_RESOURCE` | 409 | Resource already exists (unique constraint) | +| `BUSINESS_RULE_VIOLATION` | 400 | Domain rule prevents operation | +| `RATE_LIMIT_EXCEEDED` | 429 | Too many requests | +| `ACCOUNT_LOCKED` | 403 | User account is locked | + +--- + +## Data Types Reference + +### PersonStatus + +```typescript +type PersonStatus = 'active' | 'inactive'; +``` + +### Username Validation + +```typescript +const USERNAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._@-]{2,49}$/; +const RESERVED_USERNAMES = ['admin', 'system', 'support', 'purposepath', 'help', 'info']; +``` + +### Default Person Types (Seeded) + +| Code | Name | Assignable | +|------|------|------------| +| EMPLOYEE | Employee | Yes | +| CONSULTANT | Consultant | Yes | +| VENDOR | Vendor | No | +| PARTNER | Partner | No | +| ADVISOR | Advisor | No | +| BOARD | Board Member | No | + +--- + +**Document End** + +[← Back to Index](./index.md) | [Part 2: Organizational Structure →](./org-structure-service.md) + ### Organizational Structure (Part 2) @@ -3020,4 +4540,4 @@ The following real-time events will be supported: **Document End** -[← Part 1: People](./people-service.md) | [Back to Index](./index.md) +[Top of document](./org-structure-service.md) | [Back to Index](./index.md) \ No newline at end of file diff --git a/docs/shared/Specifications/user-app/traction-service/README.md b/docs/shared/Specifications/api-fe/traction-service/README.md similarity index 93% rename from docs/shared/Specifications/user-app/traction-service/README.md rename to docs/shared/Specifications/api-fe/traction-service/README.md index 612655bc..a26be8ff 100644 --- a/docs/shared/Specifications/user-app/traction-service/README.md +++ b/docs/shared/Specifications/api-fe/traction-service/README.md @@ -202,6 +202,15 @@ Example: `"2025-12-23T15:30:00.000Z"` ## Version History +### v7.1 (February 25, 2026) +- Added measure-definition integration hint contract fields across available-measures and measure create/read/update responses: + - `isIntegrationEnabled` + - `supportedSystems[]` (`systemId`, `systemName`, `systemIconUrl`) + - `hasExistingCompatibleConnection` + - `existingCompatibleConnectionIds[]` + - `integrationEligibilityReason` +- Clarified tenant-scoped active compatible connection detection semantics. + ### v7.0 (December 23, 2025) - Removed deprecated Measure endpoints (GoalMeasureLink, MeasureMilestone, MeasureActual, MeasureReading) - Split monolithic spec into controller-based documents diff --git a/docs/shared/Specifications/user-app/traction-service/actions-api.md b/docs/shared/Specifications/api-fe/traction-service/actions-api.md similarity index 100% rename from docs/shared/Specifications/user-app/traction-service/actions-api.md rename to docs/shared/Specifications/api-fe/traction-service/actions-api.md diff --git a/docs/shared/Specifications/user-app/traction-service/alignment-api.md b/docs/shared/Specifications/api-fe/traction-service/alignment-api.md similarity index 100% rename from docs/shared/Specifications/user-app/traction-service/alignment-api.md rename to docs/shared/Specifications/api-fe/traction-service/alignment-api.md diff --git a/docs/shared/Specifications/user-app/traction-service/dashboard-reports-activities-api.md b/docs/shared/Specifications/api-fe/traction-service/dashboard-reports-activities-api.md similarity index 100% rename from docs/shared/Specifications/user-app/traction-service/dashboard-reports-activities-api.md rename to docs/shared/Specifications/api-fe/traction-service/dashboard-reports-activities-api.md diff --git a/docs/shared/Specifications/user-app/traction-service/goals-api.md b/docs/shared/Specifications/api-fe/traction-service/goals-api.md similarity index 100% rename from docs/shared/Specifications/user-app/traction-service/goals-api.md rename to docs/shared/Specifications/api-fe/traction-service/goals-api.md diff --git a/docs/shared/Specifications/user-app/traction-service/insights-api.md b/docs/shared/Specifications/api-fe/traction-service/insights-api.md similarity index 100% rename from docs/shared/Specifications/user-app/traction-service/insights-api.md rename to docs/shared/Specifications/api-fe/traction-service/insights-api.md diff --git a/docs/shared/Specifications/api-fe/traction-service/integration-service.md b/docs/shared/Specifications/api-fe/traction-service/integration-service.md new file mode 100644 index 00000000..d50e8b07 --- /dev/null +++ b/docs/shared/Specifications/api-fe/traction-service/integration-service.md @@ -0,0 +1,457 @@ +# Integration Service API Specification (User App) + +**Service:** Integration Service +**Base Path:** `/integration` +**Version:** 1.1 +**Last Updated:** February 19, 2026 + +## Overview + +The Integration Service API enables end-user integration workflows in Settings and Measure screens. +This specification covers **user-facing** endpoints only (no admin metadata-definition endpoints). + +### Contract Source of Truth + +- Backend async Integration AI+CData event contracts are specified in [../integration/async-integration-ai-cdata-contracts.md](../../integration/async-integration-ai-cdata-contracts.md). +- This frontend spec owns UX-facing request/response behavior and client handling expectations. +- Backend event schema changes must be reflected in both docs in the same change set. + +### Key Capabilities + +- Connected systems list with status/diagnostics and measure counts +- Add-system bootstrap (provider subaccount + connection persistence) +- Measure integration create/edit/delete with eligibility checks +- Snapshot/aggregate scheduling controls and frequency rules +- Parameter selection with optional lookup values (`valueKey` + `valueName`) +- Test-first lifecycle and save gating based on tested-state validity + +--- + +## Authentication & Headers + +```http +Authorization: Bearer {accessToken} +X-Tenant-Id: {tenantId} +Content-Type: application/json +``` + +All endpoints are tenant-scoped and require authenticated user context. + +--- + +## Endpoint Summary + +| Method | Endpoint | Description | +|---|---|---| +| GET | `/connections` | List connected systems with aggregate integration data | +| GET | `/connections/eligible-systems` | Systems user can add now based on eligible measures | +| POST | `/connections/bootstrap` | Ensure tenant-provider/subaccount bootstrap exists | +| POST | `/connections` | Persist system connection returned by provider flow | +| PATCH | `/connections/{connectionId}` | Edit connection display/settings details | +| POST | `/connections/{connectionId}/test` | Test connection and refresh health status | +| GET | `/connections/{connectionId}/measures` | List integrations under a connection | +| GET | `/measure-integrations/eligible-measures` | List measures eligible for integration for selected system | +| GET | `/measure-integrations/{integrationId}` | Get integration details for edit/view | +| POST | `/measure-integrations` | Create integration (save-gated by tested state) | +| PUT | `/measure-integrations/{integrationId}` | Update integration (save-gated by tested state rules) | +| DELETE | `/measure-integrations/{integrationId}` | Delete integration | +| POST | `/measure-integrations/{integrationId}/test` | Test retrieval only (no measure-data persistence) | + +--- + +## Common Data Models + +### ConnectionSummaryResponse + +```typescript +interface ConnectionSummaryResponse { + connectionId: string; // UUID + systemId: string; // UUID + systemName: string; + systemCategory?: string; + systemIconUrl?: string; + connectedMeasureCount: number; // >= 0 + connectionStatus: 'Healthy' | 'Degraded' | 'Invalid' | 'Expired'; + lastHealthCheckAtUtc?: string; // ISO-8601 UTC + lastConnectionErrorCode?: string; + lastConnectionErrorAtUtc?: string; // ISO-8601 UTC +} +``` + +### MeasureIntegrationResponse + +```typescript +interface MeasureIntegrationResponse { + integrationId: string; // UUID + measureId: string; // UUID + measureName: string; + connectionId: string; // UUID + systemId: string; // UUID + systemName: string; + + isEnabled: boolean; + testStatus: 'NotTested' | 'Tested' | 'Invalidated'; + testedAtUtc?: string; + testFingerprint?: string; + + measureType: 'quantitative'; + measureDataType: 'snapshot' | 'aggregate'; + aggregationType?: 'sum' | 'average' | 'count' | 'min' | 'max'; + aggregationPeriod?: 'day' | 'week' | 'month' | 'quarter' | 'year'; + measureIntent: string; + + dataCalculationMethod?: 'previousPeriod' | 'movingAverage'; + frequencyValue: number; // integer > 0 + frequencyUnit: 'days' | 'months'; + lagDaysAfterPeriodEnd?: number; // integer >= 0 + measureTimeZone: string; // IANA timezone + + parameters: IntegrationParameterSelection[]; + + lastReadingAtUtc?: string; + lastReadingStatus?: 'Success' | 'Failed'; + lastReadingReason?: string; +} + +interface IntegrationParameterSelection { + parameterId: string; + parameterName: string; + sourceColumnName: string; + dataType: 'string' | 'number' | 'boolean' | 'date' | 'datetime'; + enabled: boolean; + selectedValueKey?: string; + selectedValueName?: string; +} +``` + +### ApiResponse Envelope + +```typescript +interface ApiResponse { + success: boolean; + data: T | null; + message?: string | null; + error?: string | null; + code?: string | null; + details?: unknown; +} +``` + +--- + +## Endpoint Details + +### 1) List Connected Systems + +**GET** `/connections` + +Returns connected systems and top-level status for Settings list view. + +#### Query Parameters + +```typescript +interface GetConnectionsParams { + includeMeasureCounts?: boolean; // default: true + includeDiagnostics?: boolean; // default: true +} +``` + +#### Response (200) + +```json +{ + "success": true, + "data": [ + { + "connectionId": "3d1c3f8d-b00d-4d1f-b634-40f814f9a70f", + "systemId": "f93c4bb0-e4b3-4a26-a770-8142e31d0c12", + "systemName": "Salesforce", + "systemCategory": "CRM", + "systemIconUrl": "https://cdn.purposepath.app/icons/salesforce.svg", + "connectedMeasureCount": 4, + "connectionStatus": "Healthy", + "lastHealthCheckAtUtc": "2026-02-19T08:02:11Z" + } + ] +} +``` + +#### Constraints + +- Returns only current tenant records. +- `connectedMeasureCount` includes active and disabled integrations. + +--- + +### 2) List Eligible Systems for Add-System + +**GET** `/connections/eligible-systems` + +Returns systems that can currently be added, based on at least one eligible measure. + +#### Response (200) + +```json +{ + "success": true, + "data": { + "canAddSystem": true, + "reasonIfDisabled": null, + "systems": [ + { + "systemId": "f93c4bb0-e4b3-4a26-a770-8142e31d0c12", + "systemName": "Salesforce", + "systemIconUrl": "https://cdn.purposepath.app/icons/salesforce.svg", + "eligibleMeasureCount": 3 + } + ] + } +} +``` + +#### Constraints + +- `canAddSystem=false` when no active quantitative measures linked to active goals are eligible. + +--- + +### 3) Bootstrap Tenant Provider/Subaccount + +**POST** `/connections/bootstrap` + +Ensures tenant-provider bootstrap exists before starting provider connection flow. + +#### Request Body + +```typescript +interface BootstrapRequest { + providerCode: 'cdata'; +} +``` + +#### Response (200) + +```json +{ + "success": true, + "data": { + "providerCode": "cdata", + "tenantProviderAccountId": "5520a223-8981-40ec-abf3-df00d8765f4a", + "subaccountCreated": true + } +} +``` + +#### Constraints + +- Idempotent: repeated calls return existing bootstrap without duplicates. + +--- + +### 4) Create Connection + +**POST** `/connections` + +Persists a provider connection after user completes provider-hosted connect UX. + +#### Request Body + +```typescript +interface CreateConnectionRequest { + systemId: string; // required UUID + providerCode: 'cdata'; + externalConnectionId: string; // required + workspaceContext?: string; + displayName?: string; // max 120 chars +} +``` + +#### Response (201) + +Returns `ConnectionSummaryResponse`. + +#### Validation Rules + +- `systemId` must reference active supported system. +- `externalConnectionId` required and non-empty. + +--- + +### 5) Test Connection + +**POST** `/connections/{connectionId}/test` + +Runs connection health test and updates diagnostics. + +#### Response (200) + +```json +{ + "success": true, + "data": { + "connectionId": "3d1c3f8d-b00d-4d1f-b634-40f814f9a70f", + "connectionStatus": "Healthy", + "lastHealthCheckAtUtc": "2026-02-19T08:22:05Z", + "errorCode": null + } +} +``` + +--- + +### 6) List Measures under Connection + +**GET** `/connections/{connectionId}/measures` + +Used by expandable row in Connected Systems screen. + +#### Response (200) + +Returns `MeasureIntegrationResponse[]` (list view projection). + +--- + +### 7) List Eligible Measures for System + +**GET** `/measure-integrations/eligible-measures?systemId={systemId}` + +Returns measures eligible for new integration under selected system. + +#### Constraints + +Eligible measure must be: +- active +- linked to at least one active goal +- catalog-based and supported by selected system +- not already connected to another system +- quantitative + +--- + +### 8) Create Measure Integration + +**POST** `/measure-integrations` + +#### Request Body + +```typescript +interface CreateMeasureIntegrationRequest { + measureId: string; // required UUID + connectionId: string; // required UUID + isEnabled: boolean; + + dataCalculationMethod?: 'previousPeriod' | 'movingAverage'; + frequencyValue: number; // integer > 0 + frequencyUnit: 'days' | 'months'; + lagDaysAfterPeriodEnd?: number; // integer >= 0 + measureTimeZone: string; // IANA timezone + + parameters: Array<{ + parameterId: string; + enabled: boolean; + selectedValueKey?: string; + selectedValueName?: string; + }>; + + testFingerprint: string; // required for create (successful test execution id/fingerprint) +} +``` + +#### Validation Rules + +- Enabled parameters must include both `selectedValueKey` and `selectedValueName`. +- `lagDaysAfterPeriodEnd` required when `dataCalculationMethod=previousPeriod` for aggregate measures. +- Create is rejected without valid tested state/fingerprint. +- Save-gating failure returns deterministic code: `TEST_REQUIRED_BEFORE_CREATE`. + +#### Response (201) + +Returns `MeasureIntegrationResponse`. + +--- + +### 9) Update Measure Integration + +**PUT** `/measure-integrations/{integrationId}` + +Same payload model as create. + +`testFingerprint` is optional for update and is only required when template-affecting changes are included. + +#### Save-Gating Rules + +- Template-affecting changes require valid tested state before save: + - parameter enable/disable + - parameter value changes + - calculation method changes + - timezone changes + - period/lag semantics changes +- Frequency-only changes do not invalidate tested state. +- If template-affecting changes are submitted without a fresh `testFingerprint`, save fails with code `RETEST_REQUIRED_FOR_TEMPLATE_CHANGES`. + +--- + +### 10) Test Measure Integration + +**POST** `/measure-integrations/{integrationId}/test` + +Runs retrieval path and returns computed value + execution metadata. +Does **not** persist measure actuals/current measure value. + +#### Response (200) + +```json +{ + "success": true, + "data": { + "integrationId": "f8f8d8b7-81de-42f7-b689-c0d4b709bc3d", + "executionId": "ea619bc1-f4ac-4d75-934b-73fc912af5f0", + "success": true, + "status": "Succeeded", + "actualValue": 125.52, + "measuredAtUtc": "2026-02-19T08:30:00Z", + "windowStartUtc": "2026-01-01T00:00:00Z", + "windowEndUtc": "2026-01-31T23:59:59Z", + "dataSource": "CData", + "errorCode": null, + "errorMessage": null, + "queryMetadata": { + "templateKey": "templates/cdata/revenue/prompt.txt", + "systemType": "CData", + "connectionType": "ApiKey", + "usesExternalReference": false, + "externalProvider": null + } + } +} +``` + +--- + +## Error Responses + +### Standard Error Envelope + +```json +{ + "success": false, + "data": null, + "error": "Human-readable message", + "code": "MACHINE_READABLE_CODE", + "details": null +} +``` + +### Common Error Codes + +- `CONNECTION_NOT_FOUND` +- `SYSTEM_NOT_ELIGIBLE` +- `MEASURE_NOT_ELIGIBLE` +- `INTEGRATION_NOT_FOUND` +- `ENABLED_PARAMETER_VALUE_REQUIRED` +- `INVALID_TIMEZONE` +- `INVALID_CALCULATION_CONFIGURATION` +- `TEST_REQUIRED_BEFORE_SAVE` +- `TEST_FINGERPRINT_MISMATCH` +- `CONNECTION_TEST_FAILED` +- `EXECUTION_FAILED` diff --git a/docs/shared/Specifications/user-app/traction-service/issues-api.md b/docs/shared/Specifications/api-fe/traction-service/issues-api.md similarity index 100% rename from docs/shared/Specifications/user-app/traction-service/issues-api.md rename to docs/shared/Specifications/api-fe/traction-service/issues-api.md diff --git a/docs/shared/Specifications/user-app/traction-service/measure-data-api.md b/docs/shared/Specifications/api-fe/traction-service/measure-data-api.md similarity index 100% rename from docs/shared/Specifications/user-app/traction-service/measure-data-api.md rename to docs/shared/Specifications/api-fe/traction-service/measure-data-api.md diff --git a/docs/shared/Specifications/user-app/traction-service/measure-links-api.md b/docs/shared/Specifications/api-fe/traction-service/measure-links-api.md similarity index 100% rename from docs/shared/Specifications/user-app/traction-service/measure-links-api.md rename to docs/shared/Specifications/api-fe/traction-service/measure-links-api.md diff --git a/docs/shared/Specifications/user-app/traction-service/measures-api.md b/docs/shared/Specifications/api-fe/traction-service/measures-api.md similarity index 96% rename from docs/shared/Specifications/user-app/traction-service/measures-api.md rename to docs/shared/Specifications/api-fe/traction-service/measures-api.md index a09812c0..dce8126c 100644 --- a/docs/shared/Specifications/user-app/traction-service/measures-api.md +++ b/docs/shared/Specifications/api-fe/traction-service/measures-api.md @@ -384,6 +384,16 @@ Retrieve the Measures catalog (catalog + tenant custom) when designing a goal th "aggregationPeriod": "monthly", "calculationMethod": "Sum of all active subscription values", "isIntegrationEnabled": true, + "supportedSystems": [ + { + "systemId": "system-001", + "systemName": "Salesforce", + "systemIconUrl": "https://cdn.example.com/icons/salesforce.svg" + } + ], + "hasExistingCompatibleConnection": true, + "existingCompatibleConnectionIds": ["connection-001"], + "integrationEligibilityReason": null, "usageInfo": { "goalCount": 3, "isUsedByThisGoal": false @@ -405,6 +415,10 @@ Retrieve the Measures catalog (catalog + tenant custom) when designing a goal th "calculationMethod": "Average of all survey responses", "measureCatalogId": null, "isIntegrationEnabled": false, + "supportedSystems": [], + "hasExistingCompatibleConnection": false, + "existingCompatibleConnectionIds": [], + "integrationEligibilityReason": "non_catalog_measure", "createdAt": "2025-01-15T10:00:00.000Z", "createdBy": "user-123", "usageInfo": { @@ -1011,6 +1025,21 @@ Retrieve the Measure catalog for a tenant when designing a new goal that is not - Usage counts still reflect existing goal links in the tenant; `isUsedByThisGoal` remains `false`. - Response shape matches `GET /goals/{goalId}/available-measures` for compatibility. +#### Integration Hint Fields (Measure Definition Flows) + +The following fields are included in available-measures payloads and measure definition responses (`POST /measures`, `PUT /measures/{id}`, `GET /measures/{id}`): + +| Field | Type | Description | +|-------|------|-------------| +| `isIntegrationEnabled` | boolean | True only when integration is eligible for this measure definition. | +| `supportedSystems[]` | array | Supported active systems from `SystemMeasureConfig` + active systems catalog. | +| `supportedSystems[].systemId` | string | Integration system ID. | +| `supportedSystems[].systemName` | string | Integration system display name. | +| `supportedSystems[].systemIconUrl` | string/null | Integration system icon URL. | +| `hasExistingCompatibleConnection` | boolean | Whether tenant has at least one active compatible connection. | +| `existingCompatibleConnectionIds[]` | array | Active tenant connection IDs compatible with supported systems. | +| `integrationEligibilityReason` | string/null | Deterministic reason when integration is not eligible (e.g., `non_catalog_measure`, `non_quantitative_measure`, `no_supported_systems`, `catalog_integration_disabled`). | + --- ### 8. Get Measure Linked Goals (Deprecated) diff --git a/docs/shared/Specifications/user-app/traction-service/strategies-api.md b/docs/shared/Specifications/api-fe/traction-service/strategies-api.md similarity index 100% rename from docs/shared/Specifications/user-app/traction-service/strategies-api.md rename to docs/shared/Specifications/api-fe/traction-service/strategies-api.md diff --git a/docs/shared/Specifications/eventbridge/async-coaching-message-events.md b/docs/shared/Specifications/integration/async-coaching-message-events.md similarity index 100% rename from docs/shared/Specifications/eventbridge/async-coaching-message-events.md rename to docs/shared/Specifications/integration/async-coaching-message-events.md diff --git a/docs/shared/Specifications/integration/async-integration-ai-cdata-contracts.md b/docs/shared/Specifications/integration/async-integration-ai-cdata-contracts.md new file mode 100644 index 00000000..86a322c4 --- /dev/null +++ b/docs/shared/Specifications/integration/async-integration-ai-cdata-contracts.md @@ -0,0 +1,192 @@ +# Integration Async Contracts (Backend) - AI + CData + +**Version:** 2.0.0 +**Date:** March 1, 2026 +**Scope:** Backend Integration contracts only (no frontend coaching UX guidance) + +## Purpose + +This document is the backend contract source of truth for asynchronous Integration orchestration between: + +- Integration Service (backend) +- AI template generation service +- CData execution path + +It defines event schemas, lifecycle transitions, retryability semantics, idempotency, and tenant/security constraints. + +## Cross-References + +- Frontend/user-facing Integration service behavior: [../api-fe/traction-service/integration-service.md](../api-fe/traction-service/integration-service.md) +- Legacy coaching async frontend spec: [async-coaching-message-events.md](async-coaching-message-events.md) +- Architecture/process standards: [../../../standards/DEVELOPMENT_GUIDELINES.md](../../../standards/DEVELOPMENT_GUIDELINES.md) + +--- + +## Event Contracts + +### SQL Template Generation - Requested + +- Detail type: `integration.sql.template.generate.requested` +- Source: `purposepath.integration` +- Envelope: `EventBridgeEnvelope` +- Contract version: `1.2` + +Required identity fields in `detail`: + +- `tenantId` +- `measureIntegrationId` +- `generationId` +- `idempotencyKey` (format: `tenant:{tenantId}|measureIntegration:{integrationId}|generation:{generationId}`) + +### SQL Template Generation - Completed + +- Detail type: `integration.sql.template.generate.completed` +- Source: `purposepath.integration.ai` +- Envelope: `EventBridgeEnvelope` + +Behavior: + +- Marks integration template generation state as `Completed` +- Stores generated template and template hash +- Clears last generation error metadata + +### SQL Template Generation - Failed + +- Detail type: `integration.sql.template.generate.failed` +- Source: `purposepath.integration.ai` +- Envelope: `EventBridgeEnvelope` + +Behavior: + +- Marks integration template generation state as `Failed` +- Persists deterministic generation error metadata (`errorCode`, `errorStage`, `message`) + +--- + +## Execution Queue Contracts + +### Execution Requested (SQS) + +Contract: `MeasureExecutionRequestMessage` + +Required identity fields: + +- `executionId` +- `integrationId` +- `tenantId` + +### Execution Result (SQS) + +Contract: `MeasureExecutionResultMessage` + +Failure classification fields: + +- `retryable` (bool) +- `failureClass` (`RETRYABLE`, `NON_RETRYABLE`, `CIRCUIT_BREAKER`) +- `errorCode` (deterministic string) +- `errorMessage` (sanitized) + +Current deterministic classes: + +- Retryable transient failures (`failureClass=RETRYABLE`) +- Non-retryable contract/auth/resource failures (`failureClass=NON_RETRYABLE`) +- Circuit-breaker state (`failureClass=CIRCUIT_BREAKER`) when explicitly emitted + +--- + +## Lifecycle and Terminal Rules + +### SQL Template Generation Lifecycle + +Allowed lifecycle states: + +- `Pending` +- `Requested` +- `Completed` +- `Failed` + +Rules: + +1. Exactly one terminal outcome per `(measureIntegrationId, generationId)` identity. +2. Duplicate terminal event with same terminal state is idempotent and ignored. +3. Conflicting terminal event for same generation id (e.g., completed then failed) is rejected. +4. Tenant mismatch between event and aggregate is rejected. + +### Execution Lifecycle + +Execution outcomes are processed idempotently by `executionId` and `dataSource`: + +1. Duplicate execution results are ignored. +2. Retryable failures schedule retry. +3. Non-retryable failures block auto-execution. +4. Circuit-breaker opens after repeated retryable failures and blocks auto-execution. + +--- + +## Retryability Semantics + +### Template Generation Failed Contract + +For `SqlTemplateGenerationFailedDetail`: + +- `retryable=true` requires `retryAfterSeconds > 0` +- `retryable=false` requires `retryAfterSeconds = null` +- `errorCode` and `errorStage` must be from canonical constant sets + +### Execution Result Contract + +`retryable` and `failureClass` must agree semantically: + +- `RETRYABLE` -> `retryable=true` +- `NON_RETRYABLE` -> `retryable=false` +- `CIRCUIT_BREAKER` -> terminal block/no auto-retry + +--- + +## Deterministic Error Taxonomy + +### Template Generation + +Canonical values are defined in `SqlTemplateGenerationContractConstants`: + +- Error codes: `SQL_POLICY_VIOLATION`, `SQL_VALIDATION_FAILED`, `SCHEMA_DISCOVERY_FAILED`, `MCP_TOOL_ERROR`, `AI_OUTPUT_INVALID`, `CDATA_AUTH_INVALID`, `CDATA_CONNECTION_NOT_FOUND`, `CDATA_RATE_LIMITED`, `INTERNAL_UNHANDLED` +- Error stages: `DISCOVER`, `GENERATE`, `VALIDATE`, `REPAIR`, `PUBLISH` + +### Execution Processing + +Deterministic execution error codes include: + +- `EXECUTION_ARGUMENT_INVALID` +- `EXECUTION_CONTRACT_INVALID` +- `EXECUTION_RESOURCE_NOT_FOUND` +- `EXECUTION_INVALID_OPERATION` +- `EXECUTION_TRANSIENT` +- `EXECUTION_FAILED` +- `CDATA_AUTH_INVALID` + +--- + +## Security and Tenant Isolation + +1. Tenant identity is validated before applying terminal events. +2. Credentials must not be published in events. +3. Logs and emitted failure messages must be sanitized for sensitive tokens/secrets. +4. Connection/auth failures are represented by deterministic error codes, not raw secret payloads. + +--- + +## Implementation Verification Checklist + +- [x] Event schemas implementation-verified in code and tests +- [x] Terminal dedupe and conflicting terminal behavior covered in unit tests +- [x] Retryable/non-retryable semantics validated for failed template events +- [x] Execution failure class represented explicitly in queue contract +- [x] API parity fields for lifecycle/error observability exposed in active integration response +- [x] Cross-reference added to frontend integration-service specification + +--- + +## Notes + +- This file intentionally excludes frontend coaching-specific guidance. +- Frontend/client behavior belongs in [../api-fe/traction-service/integration-service.md](../api-fe/traction-service/integration-service.md). diff --git a/docs/shared/Specifications/user-app/people-service.md b/docs/shared/Specifications/user-app/people-service.md deleted file mode 100644 index 8c8bd382..00000000 --- a/docs/shared/Specifications/user-app/people-service.md +++ /dev/null @@ -1,1488 +0,0 @@ -# People & Organizational Structure - Backend Integration Specifications (Part 1: People) - -**Version:** 2.0 -**Last Updated:** December 26, 2025 -**Service Base URL:** `{REACT_APP_ACCOUNT_API_URL}` (People & Auth endpoints) -**Default (Localhost):** `http://localhost:8001` (Account) - -[← Back to Index](./index.md) | [Part 2: Organizational Structure →](./org-structure-service.md) - -## Changelog - -| Version | Date | Changes | -|---------|------|----------| -| 2.0 | December 26, 2025 | **BREAKING:** Converted all JSON properties from snake_case to camelCase to match C#/.NET implementation (e.g., `person_id` → `personId`, `first_name` → `firstName`). Query parameters also converted to camelCase. This matches ASP.NET Core default JSON serialization. | -| 1.1 | December 23, 2025 | Migrated People endpoints from Traction service to Account service | -| 1.0 | December 21, 2025 | Initial version | - ---- - -## Overview - -The People module manages all person records within a tenant, including employees, consultants, vendors, and other stakeholders. This module also handles Person Types, Tags, and the User-Person linking that enables work item assignments. - -### Frontend Implementation - -- **Primary Client:** `accountClient` (axios instance) ⚠️ *Changed from tractionClient* -- **Related Files:** - - `src/services/people-service.ts` - Person CRUD operations - - `src/services/person-types-service.ts` - Person type management - - `src/services/person-tags-service.ts` - Tag management - -### Key Concepts - -- **Person**: Any individual related to the business (may or may not have system access) -- **User**: A Person with PurposePath login credentials -- **PersonType**: Category defining relationship to business (Employee, Vendor, etc.) -- **Tag**: Flexible label for grouping/filtering people -- **Assignable**: Flag indicating if a person can receive work assignments - -### Email Verification - -A Person's `isEmailVerified` flag is set to `true` when: -1. They accept a user invitation and create their account (primary flow) -2. They complete email verification during password reset - -There is no explicit `POST /people/{id}/verify-email` endpoint. Email verification is implicitly handled through the user invitation acceptance flow. - ---- - -## Authentication Changes - -### POST /auth/login - -User authentication with username/password. - -**⚠️ BREAKING CHANGE:** Login now uses `username` instead of `email`. - -**Request:** - -```json -{ - "username": "string", - "password": "string" -} -``` - -**Field Constraints:** - -| Field | Type | Required | Constraints | -|-------|------|----------|-------------| -| username | string | Yes | 3-50 characters | -| password | string | Yes | Min 8 characters | - -**Response:** - -```json -{ - "success": true, - "data": { - "accessToken": "string", - "refreshToken": "string", - "user": { - "userId": "string (GUID)", - "username": "string", - "personId": "string (GUID)", - "tenantId": "string (GUID)", - "avatarUrl": "string?", - "createdAt": "string (ISO 8601)", - "updatedAt": "string (ISO 8601)", - "status": "active | inactive | locked", - "preferences": {} - }, - "person": { - "id": "string (GUID)", - "firstName": "string", - "lastName": "string", - "email": "string?", - "phone": "string?", - "title": "string?" - }, - "tenant": { - "id": "string (GUID)", - "name": "string" - } - } -} -``` - -**Error Responses:** - -```json -{ - "success": false, - "error": "Invalid username or password", - "code": "INVALID_CREDENTIALS" -} -``` - -```json -{ - "success": false, - "error": "Account is locked", - "code": "ACCOUNT_LOCKED", - "details": { - "lockedUntil": "2025-12-22T10:30:00Z" - } -} -``` - -**Frontend Handling:** - -- Stores `accessToken` → `localStorage.accessToken` -- Stores `refreshToken` → `localStorage.refreshToken` -- Stores `tenant.id` → `localStorage.tenantId` -- Person data available for profile display - ---- - -### POST /auth/forgot-username - -Request username reminder via email. - -**Request:** - -```json -{ - "email": "string" -} -``` - -**Field Constraints:** - -| Field | Type | Required | Constraints | -|-------|------|----------|-------------| -| email | string | Yes | Valid email format | - -**Response (Always 200 to prevent email enumeration):** - -```json -{ - "success": true, - "message": "If the email is associated with any accounts, instructions have been sent." -} -``` - -**Backend Behavior:** - -1. Find all Persons with verified email matching input -2. For each Person linked to a User, include the username -3. Send email listing all associated usernames and tenant names -4. Always return success (even if email not found) - ---- - -### PUT /user/username - -Change current user's username. - -**Headers Required:** - -- `Authorization: Bearer {accessToken}` -- `X-Tenant-Id: {tenantId}` - -**Request:** - -```json -{ - "newUsername": "string", - "currentPassword": "string" -} -``` - -**Field Constraints:** - -| Field | Type | Required | Constraints | -|-------|------|----------|-------------| -| newUsername | string | Yes | 3-50 chars, alphanumeric + `.` `_` `-` `@`, must start with alphanumeric | -| currentPassword | string | Yes | Current password for verification | - -**Response:** - -```json -{ - "success": true, - "data": { - "username": "string", - "previousUsername": "string", - "nextChangeAllowedAt": "string (ISO 8601)" - } -} -``` - -**Error Responses:** - -```json -{ - "success": false, - "error": "Username is already taken", - "code": "DUPLICATE_RESOURCE", - "details": { - "field": "newUsername" - } -} -``` - -```json -{ - "success": false, - "error": "Username can only be changed once every 30 days", - "code": "RATE_LIMIT_EXCEEDED", - "details": { - "nextChangeAllowedAt": "2025-01-15T10:30:00Z" - } -} -``` - -```json -{ - "success": false, - "error": "Invalid current password", - "code": "INVALID_CREDENTIALS" -} -``` - -**Business Rules:** - -- Username must be globally unique (case-insensitive) -- Maximum one change per 30 days -- Previous username reserved for 90 days (cannot be claimed by others) -- Email notification sent to linked Person's email - ---- - -## People Endpoints - -### GET /people - -List people with filtering and pagination. - -**Headers Required:** - -- `Authorization: Bearer {accessToken}` -- `X-Tenant-Id: {tenantId}` - -**Query Parameters:** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| status | string | `active` | Filter: `active`, `inactive`, `all` | -| isAssignable | boolean | - | Filter by assignable flag | -| personTypeId | string (GUID) | - | Filter by person type | -| tags | string | - | Comma-separated tag GUIDs (any match) | -| search | string | - | Search by name, email, or title | -| page | number | 1 | Page number (1-based) | -| pageSize | number | 20 | Items per page (max 100) | -| sortBy | string | `name` | Sort field: `name`, `createdAt`, `type` | -| sortOrder | string | `asc` | Sort order: `asc`, `desc` | - -**Response:** - -```json -{ - "success": true, - "data": { - "items": [ - { - "id": "string (GUID)", - "firstName": "string", - "lastName": "string", - "email": "string?", - "isEmailVerified": "boolean", - "phone": "string?", - "title": "string?", - "personType": { - "id": "string (GUID)", - "code": "string", - "name": "string" - }, - "isActive": "boolean", - "isAssignable": "boolean", - "primaryRole": { - "id": "string (GUID)", - "name": "string" - }, - "tags": [ - { - "id": "string (GUID)", - "name": "string" - } - ], - "hasSystemAccess": "boolean", - "createdAt": "string (ISO 8601)", - "updatedAt": "string (ISO 8601)?" - } - ], - "pagination": { - "page": "number", - "pageSize": "number", - "totalItems": "number", - "totalPages": "number" - } - } -} -``` - ---- - -### GET /people/assignable - -Get list of people available for work assignment dropdowns. - -**Headers Required:** - -- `Authorization: Bearer {accessToken}` -- `X-Tenant-Id: {tenantId}` - -**Query Parameters:** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| tags | string | - | Comma-separated tag GUIDs (any match) | -| personTypeId | string (GUID) | - | Filter by person type | -| search | string | - | Search by name | - -**Response:** - -```json -{ - "success": true, - "data": [ - { - "id": "string (GUID)", - "name": "string", - "title": "string?", - "primaryRole": "string?", - "isCurrentUser": "boolean" - } - ] -} -``` - -**Notes:** - -- Returns only people where `isActive = true` AND `isAssignable = true` -- `isCurrentUser` is `true` if this Person is linked to the authenticated User -- Sorted alphabetically by name, with current user first - ---- - -### GET /people/{id} - -Get detailed person information. - -**Path Parameters:** - -- `id` - Person ID (GUID) - -**Headers Required:** - -- `Authorization: Bearer {accessToken}` -- `X-Tenant-Id: {tenantId}` - -**Response:** - -```json -{ - "success": true, - "data": { - "id": "string (GUID)", - "firstName": "string", - "lastName": "string", - "email": "string?", - "isEmailVerified": "boolean", - "phone": "string?", - "title": "string?", - "personType": { - "id": "string (GUID)", - "code": "string", - "name": "string" - }, - "isActive": "boolean", - "isAssignable": "boolean", - "notes": "string?", - "tags": [ - { - "id": "string (GUID)", - "name": "string" - } - ], - "roles": [ - { - "id": "string (GUID)", - "roleId": "string (GUID)", - "roleCode": "string", - "roleName": "string", - "isPrimary": "boolean", - "effectiveDate": "string (ISO 8601)", - "terminationDate": "string (ISO 8601)?" - } - ], - "linkedUserId": "string (GUID)?", - "hasSystemAccess": "boolean", - "createdAt": "string (ISO 8601)", - "updatedAt": "string (ISO 8601)?", - "createdBy": "string (GUID)", - "updatedBy": "string (GUID)?" - } -} -``` - -**Error Responses:** - -```json -{ - "success": false, - "error": "Person not found", - "code": "RESOURCE_NOT_FOUND", - "details": { - "resourceType": "Person", - "resourceId": "guid" - } -} -``` - ---- - -### POST /people - -Create a new person. - -**Headers Required:** - -- `Authorization: Bearer {accessToken}` -- `X-Tenant-Id: {tenantId}` - -**Request:** - -```json -{ - "firstName": "string", - "lastName": "string", - "email": "string?", - "phone": "string?", - "title": "string?", - "personTypeId": "string (GUID)", - "isAssignable": "boolean?", - "notes": "string?", - "tags": ["string (GUID)"] -} -``` - -**Field Constraints:** - -| Field | Type | Required | Constraints | -|-------|------|----------|-------------| -| firstName | string | Yes | 1-100 characters | -| lastName | string | Yes | 1-100 characters | -| email | string | No* | Valid email format, unique within tenant | -| phone | string | No | Max 20 characters | -| title | string | No | Max 100 characters | -| personTypeId | GUID | Yes | Must exist and be active | -| isAssignable | boolean | No | Defaults from PersonType.isAssignableByDefault | -| notes | string | No | Max 2000 characters | -| tags | GUID[] | No | Must be valid tag IDs | - -*Email is required if person will be linked to a User. - -**Response:** - -```json -{ - "success": true, - "data": { - "id": "string (GUID)", - "firstName": "string", - "lastName": "string", - "email": "string?", - "isEmailVerified": false, - "phone": "string?", - "title": "string?", - "personType": { - "id": "string (GUID)", - "code": "string", - "name": "string" - }, - "isActive": true, - "isAssignable": "boolean", - "notes": "string?", - "tags": [], - "roles": [], - "linkedUserId": null, - "hasSystemAccess": false, - "createdAt": "string (ISO 8601)", - "createdBy": "string (GUID)" - } -} -``` - -**Error Responses:** - -```json -{ - "success": false, - "error": "Email is already in use within this tenant", - "code": "DUPLICATE_RESOURCE", - "details": { - "field": "email" - } -} -``` - -```json -{ - "success": false, - "error": "Person type not found or inactive", - "code": "VALIDATION_ERROR", - "details": { - "field": "personTypeId" - } -} -``` - ---- - -### PUT /people/{id} - -Update an existing person. - -**Path Parameters:** - -- `id` - Person ID (GUID) - -**Headers Required:** - -- `Authorization: Bearer {accessToken}` -- `X-Tenant-Id: {tenantId}` - -**Request:** - -```json -{ - "firstName": "string?", - "lastName": "string?", - "email": "string?", - "phone": "string?", - "title": "string?", - "personTypeId": "string (GUID)?", - "isAssignable": "boolean?", - "notes": "string?" -} -``` - -**Notes:** - -- All fields are optional (partial update) -- Cannot update `isActive` via this endpoint (use activate/deactivate) -- Tags managed via separate endpoints - -**Response:** - -```json -{ - "success": true, - "data": "PersonResponse (full object)" -} -``` - ---- - -### DELETE /people/{id} - -Soft delete (deactivate) a person. Optionally reassign their work items to another person. - -**Path Parameters:** - -- `id` - Person ID (GUID) - -**Query Parameters:** - -- `reassignTo` - Optional: Person ID (GUID) to reassign work items to - -**Headers Required:** - -- `Authorization: Bearer {accessToken}` -- `X-Tenant-Id: {tenantId}` - -**Response:** - -```json -{ - "success": true, - "message": "Person deactivated successfully", - "data": { - "id": "string (GUID)", - "firstName": "string", - "lastName": "string", - "displayName": "string", - "email": "string?", - "phone": "string?", - "title": "string?", - "personType": { - "id": "string (GUID)", - "name": "string" - }, - "isAssignable": false, - "isPrimary": false, - "notes": "string?", - "status": "inactive", - "linkedUserId": "string (GUID)?", - "isEmailVerified": false, - "tags": ["string (GUID)"], - "roleAssignments": [], - "createdBy": "string (GUID)", - "createdAt": "string (ISO 8601)", - "updatedBy": "string (GUID)", - "updatedAt": "string (ISO 8601)" - } -} -``` - -**Error Responses:** - -- **404 Not Found** - Person not found -- **400 Bad Request** - Invalid person ID format, invalid reassignTo ID, or deactivation failed -- **500 Internal Server Error** - Server error - ---- - -### POST /people/{id}/activate - -Reactivate a deactivated person. - -**Path Parameters:** - -- `id` - Person ID (GUID) - -**Headers Required:** - -- `Authorization: Bearer {accessToken}` -- `X-Tenant-Id: {tenantId}` - -**Response:** - -```json -{ - "success": true, - "message": "Person activated successfully", - "data": { - "id": "string (GUID)", - "firstName": "string", - "lastName": "string", - "displayName": "string", - "email": "string?", - "phone": "string?", - "title": "string?", - "personType": { - "id": "string (GUID)", - "name": "string" - }, - "isAssignable": true, - "isPrimary": false, - "notes": "string?", - "status": "active", - "linkedUserId": "string (GUID)?", - "isEmailVerified": false, - "tags": ["string (GUID)"], - "roleAssignments": [], - "createdBy": "string (GUID)", - "createdAt": "string (ISO 8601)", - "updatedBy": "string (GUID)", - "updatedAt": "string (ISO 8601)" - } -} -``` - -**Error Responses:** - -- **404 Not Found** - Person not found -- **400 Bad Request** - Invalid person ID format or activation failed -- **500 Internal Server Error** - Server error - ---- - -### POST /people/{id}/link-user - -Link a person to an existing user account. - -**Path Parameters:** - -- `id` - Person ID (GUID) - -**Headers Required:** - -- `Authorization: Bearer {accessToken}` -- `X-Tenant-Id: {tenantId}` - -**Request:** - -```json -{ - "userId": "string (GUID)" -} -``` - -**Response:** - -```json -{ - "success": true, - "data": { - "personId": "string (GUID)", - "userId": "string (GUID)", - "linkedAt": "string (ISO 8601)" - } -} -``` - -**Error Responses:** - -```json -{ - "success": false, - "error": "Person is already linked to a user", - "code": "BUSINESS_RULE_VIOLATION", - "details": { - "existingUserId": "guid" - } -} -``` - -```json -{ - "success": false, - "error": "User is already linked to another person", - "code": "BUSINESS_RULE_VIOLATION", - "details": { - "existingPersonId": "guid" - } -} -``` - -```json -{ - "success": false, - "error": "Person must have a verified email to be linked to a user", - "code": "VALIDATION_ERROR" -} -``` - ---- - -### POST /people/{id}/tags - -Add tags to a person. - -**Path Parameters:** - -- `id` - Person ID (GUID) - -**Headers Required:** - -- `Authorization: Bearer {accessToken}` -- `X-Tenant-Id: {tenantId}` - -**Request:** - -```json -{ - "tagIds": ["string (GUID)"] -} -``` - -**Response:** - -```json -{ - "success": true, - "data": { - "tags": [ - { - "id": "string (GUID)", - "name": "string", - "assignedAt": "string (ISO 8601)" - } - ] - } -} -``` - ---- - -### DELETE /people/{id}/tags/{tagId} - -Remove a tag from a person. - -**Path Parameters:** - -- `id` - Person ID (GUID) -- `tagId` - Tag ID (GUID) - -**Headers Required:** - -- `Authorization: Bearer {accessToken}` -- `X-Tenant-Id: {tenantId}` - -**Response:** - -```json -{ - "success": true -} -``` - ---- - -## Person Roles Endpoints - -### GET /people/{id}/roles - -Get person's current role assignments. - -**Path Parameters:** - -- `id` - Person ID (GUID) - -**Headers Required:** - -- `Authorization: Bearer {accessToken}` -- `X-Tenant-Id: {tenantId}` - -**Response:** - -```json -{ - "success": true, - "data": [ - { - "id": "string (GUID)", - "role": { - "id": "string (GUID)", - "code": "string", - "name": "string" - }, - "isPrimary": "boolean", - "effectiveDate": "string (ISO 8601)", - "terminationDate": null - } - ] -} -``` - ---- - -### GET /people/{id}/roles/history - -Get person's complete role assignment history. - -**Path Parameters:** - -- `id` - Person ID (GUID) - -**Headers Required:** - -- `Authorization: Bearer {accessToken}` -- `X-Tenant-Id: {tenantId}` - -**Response:** - -```json -{ - "success": true, - "data": { - "current": [ - { - "id": "string (GUID)", - "role": { - "id": "string (GUID)", - "code": "string", - "name": "string" - }, - "isPrimary": "boolean", - "effectiveDate": "string (ISO 8601)" - } - ], - "historical": [ - { - "id": "string (GUID)", - "role": { - "id": "string (GUID)", - "code": "string", - "name": "string" - }, - "isPrimary": "boolean", - "effectiveDate": "string (ISO 8601)", - "terminationDate": "string (ISO 8601)" - } - ] - } -} -``` - ---- - -### POST /people/{id}/roles - -Assign a role to a person. - -**Path Parameters:** - -- `id` - Person ID (GUID) - -**Headers Required:** - -- `Authorization: Bearer {accessToken}` -- `X-Tenant-Id: {tenantId}` - -**Request:** - -```json -{ - "roleId": "string (GUID)", - "isPrimary": "boolean?", - "effectiveDate": "string (ISO 8601)?" -} -``` - -**Field Constraints:** - -| Field | Type | Required | Constraints | -|-------|------|----------|-------------| -| roleId | GUID | Yes | Must be active role | -| isPrimary | boolean | No | Default: `true` if first role, else `false` | -| effectiveDate | ISO 8601 | No | Default: current date | - -**Response:** - -```json -{ - "success": true, - "data": { - "id": "string (GUID)", - "role": { - "id": "string (GUID)", - "code": "string", - "name": "string" - }, - "isPrimary": "boolean", - "effectiveDate": "string (ISO 8601)", - "previousOccupantTerminated": "boolean" - } -} -``` - -**Notes:** - -- If role already has an occupant, their assignment is auto-terminated -- `previousOccupantTerminated` indicates if this happened - -**Error Responses:** - -```json -{ - "success": false, - "error": "Person is not active", - "code": "BUSINESS_RULE_VIOLATION" -} -``` - -```json -{ - "success": false, - "error": "Role not found or inactive", - "code": "VALIDATION_ERROR", - "details": { - "field": "roleId" - } -} -``` - ---- - -### PUT /people/{id}/roles/{roleId}/primary - -Set a role as the person's primary role. - -**Path Parameters:** - -- `id` - Person ID (GUID) -- `roleId` - Role ID (GUID) - -**Headers Required:** - -- `Authorization: Bearer {accessToken}` -- `X-Tenant-Id: {tenantId}` - -**Response:** - -```json -{ - "success": true, - "data": { - "previousPrimaryRoleId": "string (GUID)?", - "newPrimaryRoleId": "string (GUID)" - } -} -``` - -**Error Responses:** - -```json -{ - "success": false, - "error": "Person is not assigned to this role", - "code": "VALIDATION_ERROR" -} -``` - ---- - -### DELETE /people/{id}/roles/{roleId} - -Unassign a role from a person. - -**Path Parameters:** - -- `id` - Person ID (GUID) -- `roleId` - Role ID (GUID) - -**Headers Required:** - -- `Authorization: Bearer {accessToken}` -- `X-Tenant-Id: {tenantId}` - -**Query Parameters:** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| terminationDate | ISO 8601 | now | When assignment ends | - -**Response:** - -```json -{ - "success": true, - "data": { - "terminated": true, - "terminationDate": "string (ISO 8601)", - "wasPrimary": "boolean", - "newPrimaryRoleId": "string (GUID)?" - } -} -``` - -**Notes:** - -- If this was the primary role and person has other roles, frontend should prompt to select new primary - ---- - -## Person Types Endpoints - -### GET /person-types - -List all person types for the tenant. - -**Headers Required:** - -- `Authorization: Bearer {accessToken}` -- `X-Tenant-Id: {tenantId}` - -**Query Parameters:** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| includeInactive | boolean | false | Include inactive types | - -**Response:** - -```json -{ - "success": true, - "data": [ - { - "id": "string (GUID)", - "code": "string", - "name": "string", - "description": "string?", - "isAssignableByDefault": "boolean", - "displayOrder": "number", - "isActive": "boolean", - "createdAt": "string (ISO 8601)", - "updatedAt": "string (ISO 8601)?" - } - ] -} -``` - ---- - -### GET /person-types/{id} - -Get person type details. - -**Path Parameters:** - -- `id` - PersonType ID (GUID) - -**Response:** - -```json -{ - "success": true, - "data": { - "id": "string (GUID)", - "code": "string", - "name": "string", - "description": "string?", - "isAssignableByDefault": "boolean", - "displayOrder": "number", - "isActive": "boolean", - "personCount": "number", - "createdAt": "string (ISO 8601)", - "updatedAt": "string (ISO 8601)?" - } -} -``` - ---- - -### POST /person-types - -Create a new person type. - -**Headers Required:** - -- `Authorization: Bearer {accessToken}` -- `X-Tenant-Id: {tenantId}` - -**Request:** - -```json -{ - "code": "string", - "name": "string", - "description": "string?", - "isAssignableByDefault": "boolean", - "displayOrder": "number?" -} -``` - -**Field Constraints:** - -| Field | Type | Required | Constraints | -|-------|------|----------|-------------| -| code | string | Yes | 2-20 chars, uppercase alphanumeric + underscore, unique within tenant | -| name | string | Yes | 1-100 characters | -| description | string | No | Max 500 characters | -| isAssignableByDefault | boolean | Yes | - | -| displayOrder | number | No | Default: next available | - -**Response:** - -```json -{ - "success": true, - "data": "PersonTypeResponse" -} -``` - -**Error Responses:** - -```json -{ - "success": false, - "error": "Person type code already exists", - "code": "DUPLICATE_RESOURCE", - "details": { - "field": "code" - } -} -``` - ---- - -### PUT /person-types/{id} - -Update a person type. - -**Path Parameters:** - -- `id` - PersonType ID (GUID) - -**Request:** - -```json -{ - "name": "string?", - "description": "string?", - "isAssignableByDefault": "boolean?", - "displayOrder": "number?" -} -``` - -**Notes:** - -- `code` cannot be changed after creation -- Changes to `isAssignableByDefault` do not affect existing persons - -**Response:** - -```json -{ - "success": true, - "data": "PersonTypeResponse" -} -``` - ---- - -### DELETE /person-types/{id} - -Deactivate a person type. - -**Path Parameters:** - -- `id` - PersonType ID (GUID) - -**Response:** - -```json -{ - "success": true -} -``` - -**Error Responses:** - -```json -{ - "success": false, - "error": "Cannot delete person type with existing persons", - "code": "BUSINESS_RULE_VIOLATION", - "details": { - "personCount": 15 - } -} -``` - ---- - -## Person Tags Endpoints - -### GET /api/person-tags - -List all tags for the tenant. - -**Headers Required:** - -- `Authorization: Bearer {accessToken}` -- `X-Tenant-Id: {tenantId}` - -**Response:** - -```json -{ - "success": true, - "data": [ - { - "id": "string (GUID)", - "name": "string", - "personCount": "number", - "createdAt": "string (ISO 8601)" - ---- - -### POST /person-types/{id}/activate - -Reactivate a deactivated person type. - -**Path Parameters:** - -- `id` - PersonType ID (GUID) - -**Headers Required:** - -- `Authorization: Bearer {accessToken}` -- `X-Tenant-Id: {tenantId}` - -**Response:** - -```json -{ - "success": true, - "data": { - "id": "string (GUID)", - "code": "string", - "name": "string", - "description": "string?", - "isAssignableByDefault": "boolean", - "displayOrder": "number", - "isActive": "boolean", - "createdAt": "string (ISO 8601)", - "updatedAt": "string (ISO 8601)?" - } -} -``` - -**Error Responses:** - -- **404 Not Found** - Person type not found -- **400 Bad Request** - Invalid person type ID format or activation failed -- **500 Internal Server Error** - Server error - } - ] -} -``` - ---- - -### POST /api/person-tags - -Create a new tag. - -**Headers Required:** - -- `Authorization: Bearer {accessToken}` -- `X-Tenant-Id: {tenantId}` - -**Request:** - -```json -{ - "name": "string" -} -``` - -**Field Constraints:** - -| Field | Type | Required | Constraints | -|-------|------|----------|-------------| -| name | string | Yes | 1-50 characters, unique within tenant | - -**Response:** - -```json -{ - "success": true, - "data": { - "id": "string (GUID)", - "name": "string", - "createdAt": "string (ISO 8601)" - } -} -``` - -**Error Responses:** - -```json -{ - "success": false, - "error": "Tag name already exists", - "code": "DUPLICATE_RESOURCE", - "details": { - "field": "name" - } -} -``` - ---- - -### PUT /api/person-tags/{id} - -Update a tag name. - -**Path Parameters:** - -- `id` - Tag ID (GUID) - -**Request:** - -```json -{ - "name": "string" -} -``` - -**Response:** - -```json -{ - "success": true, - "data": { - "id": "string (GUID)", - "name": "string" - } -} -``` - ---- - -### DELETE /api/person-tags/{id} - -Delete a tag (cascade removes all person-tag assignments). - -**Path Parameters:** - -- `id` - Tag ID (GUID) - -**Response:** - -```json -{ - "success": true, - "data": { - "assignmentsRemoved": "number" - } -} -``` - ---- - -## Error Codes Reference - -| Code | HTTP Status | Description | -|------|-------------|-------------| -| `VALIDATION_ERROR` | 400 | Input validation failed | -| `INVALID_CREDENTIALS` | 401 | Wrong username/password | -| `UNAUTHORIZED` | 401 | Missing/invalid token | -| `FORBIDDEN` | 403 | Insufficient permissions | -| `RESOURCE_NOT_FOUND` | 404 | Resource does not exist | -| `DUPLICATE_RESOURCE` | 409 | Resource already exists (unique constraint) | -| `BUSINESS_RULE_VIOLATION` | 400 | Domain rule prevents operation | -| `RATE_LIMIT_EXCEEDED` | 429 | Too many requests | -| `ACCOUNT_LOCKED` | 403 | User account is locked | - ---- - -## Data Types Reference - -### PersonStatus - -```typescript -type PersonStatus = 'active' | 'inactive'; -``` - -### Username Validation - -```typescript -const USERNAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._@-]{2,49}$/; -const RESERVED_USERNAMES = ['admin', 'system', 'support', 'purposepath', 'help', 'info']; -``` - -### Default Person Types (Seeded) - -| Code | Name | Assignable | -|------|------|------------| -| EMPLOYEE | Employee | Yes | -| CONSULTANT | Consultant | Yes | -| VENDOR | Vendor | No | -| PARTNER | Partner | No | -| ADVISOR | Advisor | No | -| BOARD | Board Member | No | - ---- - -**Document End** - -[← Back to Index](./index.md) | [Part 2: Organizational Structure →](./org-structure-service.md) From d0ef7b7f30fccce0622805050cf5aa58184db68e Mon Sep 17 00:00:00 2001 From: "GitHub Actions (Docs Sync)" Date: Tue, 3 Mar 2026 20:01:27 +0000 Subject: [PATCH 2/7] docs: sync shared documentation from PurposePath_Api [docs-sync] Synced from: PurposePath_Api@5f2f1375 Original commit: Merge pull request #700 from mottych/hotfix/issue- Original author: Motty Chen Branch: master [skip ci] --- .../admin-portal/admin-api-specification.md | 4815 +++++++++++++++++ .../admin_ai_specifications.md | 1127 ++++ .../ai-user/backend-integration-unified-ai.md | 3583 ++++++++++++ .../ai-user/coaching-session-workflow.md | 95 + .../async-coaching-message-events.md | 1218 +++++ .../Websocket/REALTIME_HOOKS_USAGE.md | 400 ++ .../REALTIME_WEBSOCKET_IMPLEMENTATION.md | 700 +++ .../Websocket/SSE_TO_WEBSOCKET_MIGRATION.md | 465 ++ .../Websocket/WEBSOCKET_EPIC_SUMMARY.md | 572 ++ .../WEBSOCKET_FRONTEND_INTEGRATION_SPEC.md | 1106 ++++ .../WEBSOCKET_IMPLEMENTATION_PROGRESS.md | 419 ++ .../Websocket/WEBSOCKET_MIGRATION_ANALYSIS.md | 997 ++++ .../Websocket/WEBSOCKET_SESSION_RECOVERY.md | 205 + .../Websocket/WEBSOCKET_UPDATE_EXAMPLE.md | 470 ++ .../Websocket/WEBSOCKET_UPDATE_PROTOCOL.md | 448 ++ .../Specifications/user-app/account-api.md | 596 ++ .../Specifications/user-app/account-gap.md | 910 ++++ .../user-app/business-foundation-api.md | 1283 +++++ .../user-app/common-patterns.md | 1188 ++++ .../user-app/dashboard-service.md | 1394 +++++ docs/shared/Specifications/user-app/index.md | 289 + .../user-app/org-structure-service.md | 3023 +++++++++++ .../Specifications/user-app/people-service.md | 1488 +++++ .../user-app/traction-service/README.md | 227 + .../user-app/traction-service/actions-api.md | 815 +++ .../traction-service/alignment-api.md | 244 + .../dashboard-reports-activities-api.md | 848 +++ .../user-app/traction-service/goals-api.md | 1119 ++++ .../user-app/traction-service/insights-api.md | 257 + .../user-app/traction-service/issues-api.md | 1811 +++++++ .../traction-service/measure-data-api.md | 979 ++++ .../traction-service/measure-links-api.md | 805 +++ .../user-app/traction-service/measures-api.md | 1440 +++++ .../traction-service/strategies-api.md | 395 ++ 34 files changed, 35731 insertions(+) create mode 100644 docs/shared/Specifications/admin-portal/admin-api-specification.md create mode 100644 docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md create mode 100644 docs/shared/Specifications/ai-user/backend-integration-unified-ai.md create mode 100644 docs/shared/Specifications/ai-user/coaching-session-workflow.md create mode 100644 docs/shared/Specifications/eventbridge/async-coaching-message-events.md create mode 100644 docs/shared/Specifications/user-app/Websocket/REALTIME_HOOKS_USAGE.md create mode 100644 docs/shared/Specifications/user-app/Websocket/REALTIME_WEBSOCKET_IMPLEMENTATION.md create mode 100644 docs/shared/Specifications/user-app/Websocket/SSE_TO_WEBSOCKET_MIGRATION.md create mode 100644 docs/shared/Specifications/user-app/Websocket/WEBSOCKET_EPIC_SUMMARY.md create mode 100644 docs/shared/Specifications/user-app/Websocket/WEBSOCKET_FRONTEND_INTEGRATION_SPEC.md create mode 100644 docs/shared/Specifications/user-app/Websocket/WEBSOCKET_IMPLEMENTATION_PROGRESS.md create mode 100644 docs/shared/Specifications/user-app/Websocket/WEBSOCKET_MIGRATION_ANALYSIS.md create mode 100644 docs/shared/Specifications/user-app/Websocket/WEBSOCKET_SESSION_RECOVERY.md create mode 100644 docs/shared/Specifications/user-app/Websocket/WEBSOCKET_UPDATE_EXAMPLE.md create mode 100644 docs/shared/Specifications/user-app/Websocket/WEBSOCKET_UPDATE_PROTOCOL.md create mode 100644 docs/shared/Specifications/user-app/account-api.md create mode 100644 docs/shared/Specifications/user-app/account-gap.md create mode 100644 docs/shared/Specifications/user-app/business-foundation-api.md create mode 100644 docs/shared/Specifications/user-app/common-patterns.md create mode 100644 docs/shared/Specifications/user-app/dashboard-service.md create mode 100644 docs/shared/Specifications/user-app/index.md create mode 100644 docs/shared/Specifications/user-app/org-structure-service.md create mode 100644 docs/shared/Specifications/user-app/people-service.md create mode 100644 docs/shared/Specifications/user-app/traction-service/README.md create mode 100644 docs/shared/Specifications/user-app/traction-service/actions-api.md create mode 100644 docs/shared/Specifications/user-app/traction-service/alignment-api.md create mode 100644 docs/shared/Specifications/user-app/traction-service/dashboard-reports-activities-api.md create mode 100644 docs/shared/Specifications/user-app/traction-service/goals-api.md create mode 100644 docs/shared/Specifications/user-app/traction-service/insights-api.md create mode 100644 docs/shared/Specifications/user-app/traction-service/issues-api.md create mode 100644 docs/shared/Specifications/user-app/traction-service/measure-data-api.md create mode 100644 docs/shared/Specifications/user-app/traction-service/measure-links-api.md create mode 100644 docs/shared/Specifications/user-app/traction-service/measures-api.md create mode 100644 docs/shared/Specifications/user-app/traction-service/strategies-api.md diff --git a/docs/shared/Specifications/admin-portal/admin-api-specification.md b/docs/shared/Specifications/admin-portal/admin-api-specification.md new file mode 100644 index 00000000..e3baaf48 --- /dev/null +++ b/docs/shared/Specifications/admin-portal/admin-api-specification.md @@ -0,0 +1,4815 @@ +# Admin API Specification + +**Version:** 2.2 +**Status:** Complete +**Last Updated:** February 5, 2026 +**Base URL:** `{REACT_APP_ADMIN_API_URL}/admin/api/v1` +**Default (Localhost):** `http://localhost:8003/admin/api/v1` +**Production:** `https://api.purposepath.app/admin/api/v1` + +--- + +## Revision History + +| Date | Version | Changes | Author | +|------|---------|---------|--------| +| Feb 5, 2026 | 2.2 | Added Discount Code Management (9 endpoints), User Management (5 endpoints), and Audit Log Management (4 endpoints) | System | +| Feb 5, 2026 | 2.1 | Added System Settings Management (5 endpoints) and Role Template Management (8 endpoints) | System | +| Feb 4, 2026 | 2.0 | Complete specification with all endpoints documented | System | +| Jan 22, 2026 | 1.1 | Added tenant management and discount codes | System | +| Dec 15, 2025 | 1.0 | Initial specification | System | + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Authentication](#authentication) +3. [Common Patterns](#common-patterns) +4. [Endpoints by Category](#endpoints-by-category) + - [Health & System](#health--system) + - [Authentication](#authentication-endpoints) + - [System Seeding](#system-seeding) + - [Issue Type Configuration](#issue-type-configuration) + - [Issue Status Configuration](#issue-status-configuration) + - [Email Template Management](#email-template-management) + - [Subscriber Management](#subscriber-management) + - [Plan Management](#plan-management) + - [Feature Management](#feature-management) + - [Subscription Operations](#subscription-operations) + - [Discount Code Management](#discount-code-management) + - [System Settings Management](#system-settings-management) + - [Role Template Management](#role-template-management) + - [User Management](#user-management) + - [Audit Log Management](#audit-log-management) + - [People Management](#people-management) +5. [Data Models](#data-models) +6. [Error Handling](#error-handling) + +--- + +## Overview + +The Admin API provides comprehensive administrative capabilities for managing the PurposePath platform. It includes functionality for: + +- **System Configuration**: Seeding reference data (tiers, settings, templates) +- **System Settings**: Managing operational parameters, security policies, email/payment configuration +- **Role Templates**: Managing organizational structure templates for tenants +- **Subscription Management**: Managing plans, pricing, subscribers, features, discount codes +- **Email Templates**: Creating and managing email templates +- **Issue Configuration**: Configuring issue types and statuses +- **User Support**: Trial extensions, discounts, feature grants +- **User Management**: Account operations (unlock, suspend, reactivate) +- **Audit Logging**: Comprehensive tracking and reporting of all administrative actions + +**Security**: All endpoints require Admin role authorization via JWT token with "Admin" policy claim. + +**Audit Trail**: All write operations are automatically logged with admin user ID, action details, and timestamps. + +--- + +## Authentication + +### Required Headers + +All Admin API endpoints (except auth endpoints) require: + +```http +Authorization: Bearer {admin_access_token} +Content-Type: application/json +``` + +### Admin Role Requirement + +Admins must be members of the **Portal Admins** Google Workspace group. The validation process: + +1. Admin authenticates via Google OAuth 2.0 +2. System validates Google Workspace group membership +3. JWT token is issued with "Admin" role claim +4. All subsequent requests validated against this role + +### Token Expiration + +- **Access Token**: 1 hour +- **Refresh Token**: 30 days +- Use `POST /auth/refresh` to obtain new tokens + +--- + +## Common Patterns + +### Pagination + +Paginated endpoints use consistent query parameters: + +``` +?page=1&pageSize=20 +``` + +**Response Structure:** +```json +{ + "success": true, + "data": { + "items": [...], + "pagination": { + "currentPage": 1, + "pageSize": 20, + "totalCount": 150, + "totalPages": 8 + } + } +} +``` + +**Constraints:** +- `page`: Minimum 1 (1-based indexing) +- `pageSize`: 1-100 (default: 20-50 depending on endpoint) + +### Response Envelope + +**Success Response:** +```json +{ + "success": true, + "data": { ... } +} +``` + +**Error Response:** +```json +{ + "success": false, + "error": "Human-readable error message" +} +``` + +### Seeding Results + +System seeding endpoints return: + +```json +{ + "itemsCreated": 10, + "itemsSkipped": 5, + "errors": ["Optional error message"] +} +``` + +**Pattern**: INSERT-IF-NOT-FOUND (idempotent operations) + +### Audit Logging + +All write operations automatically log: +- Admin user ID +- Action type (create, update, delete, etc.) +- Resource type and ID +- Descriptive message +- Timestamp +- IP address (from request context) + +--- + +## Endpoints by Category + +--- + +## Health & System + +### GET /health + +Health check endpoint (no authentication required). + +**Response:** +```json +{ + "status": "healthy", + "service": "PurposePath Admin Lambda", + "version": "1.2.0", + "timestamp": "2026-02-04T10:30:00Z", + "environment": "Production" +} +``` + +**Status Codes:** +- `200 OK` - Service is healthy + +--- + +## Authentication Endpoints + +### POST /auth/validate + +Validate admin user via Google OAuth and issue JWT tokens (public endpoint). + +**Request:** +```json +{ + "googleAccessToken": "string", + "email": "admin@example.com" +} +``` + +**Validations:** +- `googleAccessToken`: Required, valid Google OAuth access token +- `email`: Required, valid email format + +**Response:** +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "email": "admin@example.com", + "name": "Admin User", + "picture": null, + "role": "admin", + "createdAt": "2025-01-15T10:30:00Z", + "lastLoginAt": "2026-02-04T10:30:00Z", + "accessToken": "eyJ...", + "refreshToken": "eyJ..." + } +} +``` + +**Status Codes:** +- `200 OK` - Validation successful +- `400 Bad Request` - Invalid request format or missing fields +- `401 Unauthorized` - Invalid Google token +- `403 Forbidden` - User not in Portal Admins group +- `500 Internal Server Error` - Server error + +**Notes:** +- Validates Google Workspace group membership +- Generates JWT tokens with 1-hour expiration +- No existing JWT required (public endpoint) + +--- + +### POST /auth/refresh + +Refresh admin access token using refresh token (public endpoint). + +**Request:** +```json +{ + "refreshToken": "eyJ..." +} +``` + +**Response:** +```json +{ + "success": true, + "accessToken": "eyJ...", + "refreshToken": "eyJ...", + "expiresIn": 3600 +} +``` + +**Status Codes:** +- `200 OK` - Token refreshed successfully +- `400 Bad Request` - Missing refresh token +- `401 Unauthorized` - Invalid or expired refresh token +- `500 Internal Server Error` - Server error + +**Notes:** +- Validates existing refresh token +- Issues new access and refresh tokens +- Refresh tokens valid for 30 days + +--- + +## System Seeding + +### POST /seed + +Seed all system data (subscription tiers, issue configs, settings, templates, dashboards). + +**No Request Body** + +**Response:** +```json +{ + "subscription_tiers": { + "itemsCreated": 3, + "itemsSkipped": 0, + "errors": [] + }, + "issue_status_configs": { + "itemsCreated": 8, + "itemsSkipped": 0, + "errors": [] + }, + "settings": { + "itemsCreated": 15, + "itemsSkipped": 0, + "errors": [] + }, + "email_templates": { + "itemsCreated": 10, + "itemsSkipped": 0, + "errors": [] + }, + "issue_type_configs": { + "itemsCreated": 5, + "itemsSkipped": 0, + "errors": [] + }, + "dashboard_templates": { + "itemsCreated": 3, + "itemsSkipped": 0, + "errors": [] + } +} +``` + +**Status Codes:** +- `200 OK` - Seeding completed + +**Notes:** +- Idempotent operation (INSERT-IF-NOT-FOUND pattern) +- Safe to run multiple times +- Skips existing items +- Creates only new items +- Use individual endpoints for targeted seeding + +--- + +### POST /seed/subscription-tiers + +Seed subscription tier reference data (Basic, Professional, Enterprise). + +**No Request Body** + +**Response:** +```json +{ + "itemsCreated": 3, + "itemsSkipped": 0, + "errors": [] +} +``` + +**Seeds:** +1. **Basic** - $9.99/month, $99.99/year (5 goals, 25 actions, basic features) +2. **Professional** - $29.99/month, $299.99/year (25 goals, 150 actions, advanced features) +3. **Enterprise** - $99.99/month, $999.99/year (unlimited resources, all features) + +**Status Codes:** +- `200 OK` - Seeding completed + +--- + +### POST /seed/issue-status-configs + +Seed issue status configurations (Open, In Progress, Resolved, Closed, etc.). + +**No Request Body** + +**Response:** +```json +{ + "itemsCreated": 8, + "itemsSkipped": 0, + "errors": [] +} +``` + +**Seeds:** +- Open (category: Open) +- In Progress (category: InProgress) +- Resolved (category: Resolved) +- Closed (category: Closed) +- Blocked (category: InProgress) +- On Hold (category: InProgress) +- Cannot Reproduce (category: Closed) +- Duplicate (category: Closed) + +**Status Codes:** +- `200 OK` - Seeding completed + +--- + +### POST /seed/settings + +Seed system settings (auth, email, billing, maintenance configurations). + +**No Request Body** + +**Response:** +```json +{ + "itemsCreated": 15, + "itemsSkipped": 0, + "errors": [] +} +``` + +**Seeds:** +- Auth settings (email verification, session timeout, password requirements, OAuth) +- Email settings (from address, verification expiry) +- Billing settings (trial period, default currency) +- System settings (maintenance mode, frontend base URL) + +**Status Codes:** +- `200 OK` - Seeding completed + +--- + +### POST /seed/email-templates + +Seed email templates (verification, password reset, welcome, trial notifications, payment alerts). + +**No Request Body** + +**Response:** +```json +{ + "itemsCreated": 10, + "itemsSkipped": 0, + "errors": [] +} +``` + +**Seeds 10 Templates:** +1. `email_verification` - Email verification (authentication) +2. `password_reset` - Password reset (authentication) +3. `welcome_email` - Welcome email (onboarding) +4. `username_changed` - Username change notification (authentication) +5. `trial_ending_soon` - Trial ending in 3 days (trial) +6. `trial_ending_tomorrow` - Trial ending tomorrow (trial) +7. `trial_expired` - Trial expired (trial) +8. `payment_failed` - Payment failure (payment) +9. `payment_retry` - Payment retry attempt (payment) +10. `subscription_suspended` - Subscription suspended (payment) + +**Template Features:** +- RazorLight syntax (replaces Mustache `{{variable}}` with `@Model.Variable`) +- Structured variable definitions with types and requirements +- HTML and plain text versions +- Category-based organization +- Active by default + +**Status Codes:** +- `200 OK` - Seeding completed + +**Notes:** +- Templates use RazorLight engine +- Variables validated against schema +- Safe for repeated calls (won't overwrite existing) +- Use for initial setup or adding new templates + +--- + +### POST /seed/issue-type-configs + +Seed issue type configurations (Bug, Feature Request, Task, etc.). + +**No Request Body** + +**Response:** +```json +{ + "itemsCreated": 5, + "itemsSkipped": 0, + "errors": [] +} +``` + +**Seeds:** +- Bug (system type, red color, bug icon) +- Feature Request (system type, blue color, feature icon) +- Task (system type, green color, task icon) +- Improvement (system type, yellow color, lightbulb icon) +- Question (system type, purple color, question icon) + +**Status Codes:** +- `200 OK` - Seeding completed + +--- + +### POST /seed/dashboard-templates + +Seed dashboard templates (Default, Executive, Daily Operations). + +**No Request Body** + +**Response:** +```json +{ + "itemsCreated": 3, + "itemsSkipped": 0, + "errors": [] +} +``` + +**Seeds:** +1. **Default Dashboard** - Goals + Actions (general users) +2. **Executive Overview** - Strategic goals, alignment, performance, AI insights +3. **Daily Operations** - Actions by status, hot list, issue list, recent activity + +**Status Codes:** +- `200 OK` - Seeding completed + +--- + +## Issue Type Configuration + +System-level issue types managed by admins (tenant-level types not included). + +### GET /issue-types + +List all system issue type configurations. + +**Query Parameters:** +- `includeInactive` (boolean, optional): Include inactive types (default: false) + +**Response:** +```json +[ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "tenantId": "SYSTEM", + "name": "Bug", + "description": "Software defect or error", + "color": "#FF5252", + "icon": "bug", + "order": 1, + "isActive": true, + "isSystemType": true, + "isDefault": true, + "createdAt": "2025-01-15T10:30:00Z", + "updatedAt": "2025-01-20T14:45:00Z" + } +] +``` + +**Status Codes:** +- `200 OK` - Types retrieved successfully +- `400 Bad Request` - Invalid query parameters +- `401 Unauthorized` - Missing or invalid admin token + +--- + +### GET /issue-types/{id} + +Get a single issue type configuration by ID. + +**Path Parameters:** +- `id` (string, GUID) - Issue type ID + +**Response:** +```json +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "tenantId": "SYSTEM", + "name": "Bug", + "description": "Software defect or error", + "color": "#FF5252", + "icon": "bug", + "order": 1, + "isActive": true, + "isSystemType": true, + "isDefault": true, + "createdAt": "2025-01-15T10:30:00Z", + "updatedAt": "2025-01-20T14:45:00Z" +} +``` + +**Status Codes:** +- `200 OK` - Type retrieved successfully +- `400 Bad Request` - Invalid ID format +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Issue type not found or not a system type + +--- + +### POST /issue-types + +Create a new system issue type configuration. + +**Request:** +```json +{ + "name": "Security Issue", + "description": "Security vulnerability or concern", + "color": "#FF0000", + "icon": "shield", + "order": 10, + "isDefault": false +} +``` + +**Validations:** +- `name`: Required, 1-50 characters, unique within system types +- `description`: Optional, max 200 characters +- `color`: Required, valid hex color format (#RRGGBB) +- `icon`: Required, 1-30 characters +- `order`: Required, positive integer +- `isDefault`: Optional, boolean (only one default allowed) + +**Response:** +```json +{ + "id": "660e8400-e29b-41d4-a716-446655440000", + "tenantId": "SYSTEM", + "name": "Security Issue", + "description": "Security vulnerability or concern", + "color": "#FF0000", + "icon": "shield", + "order": 10, + "isActive": true, + "isSystemType": true, + "isDefault": false, + "createdAt": "2026-02-04T10:30:00Z", + "updatedAt": "2026-02-04T10:30:00Z" +} +``` + +**Status Codes:** +- `201 Created` - Issue type created successfully +- `400 Bad Request` - Invalid request data or duplicate name +- `401 Unauthorized` - Missing or invalid admin token + +**Notes:** +- Automatically set as system type +- Tenant ID set to "SYSTEM" +- Initially active + +--- + +### PUT /issue-types/{id} + +Update an existing system issue type configuration. + +**Path Parameters:** +- `id` (string, GUID) - Issue type ID + +**Request:** +```json +{ + "name": "Critical Bug", + "description": "High-priority software defect", + "color": "#FF0000", + "icon": "alert", + "order": 1, + "isDefault": true +} +``` + +**Validations:** +- All fields optional (partial updates supported) +- Same validation rules as POST + +**Response:** +```json +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "tenantId": "SYSTEM", + "name": "Critical Bug", + "description": "High-priority software defect", + "color": "#FF0000", + "icon": "alert", + "order": 1, + "isActive": true, + "isSystemType": true, + "isDefault": true, + "createdAt": "2025-01-15T10:30:00Z", + "updatedAt": "2026-02-04T11:00:00Z" +} +``` + +**Status Codes:** +- `200 OK` - Issue type updated successfully +- `400 Bad Request` - Invalid request data or not a system type +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Issue type not found + +--- + +### DELETE /issue-types/{id} + +Soft delete an issue type configuration (sets isActive=false). + +**Path Parameters:** +- `id` (string, GUID) - Issue type ID + +**No Request Body** + +**Response:** +``` +204 No Content +``` + +**Status Codes:** +- `204 No Content` - Issue type deactivated successfully +- `400 Bad Request` - Invalid ID or not a system type +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Issue type not found + +**Notes:** +- Soft delete only (sets isActive=false) +- Existing issues retain their type reference +- Use POST /issue-types/{id}/activate to restore + +--- + +### POST /issue-types/{id}/activate + +Activate (restore) a soft-deleted issue type. + +**Path Parameters:** +- `id` (string, GUID) - Issue type ID + +**No Request Body** + +**Response:** +```json +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "tenantId": "SYSTEM", + "name": "Bug", + "description": "Software defect or error", + "color": "#FF5252", + "icon": "bug", + "order": 1, + "isActive": true, + "isSystemType": true, + "isDefault": true, + "createdAt": "2025-01-15T10:30:00Z", + "updatedAt": "2026-02-04T11:15:00Z" +} +``` + +**Status Codes:** +- `200 OK` - Issue type activated successfully +- `400 Bad Request` - Invalid ID, not a system type, or already active +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Issue type not found + +--- + +### POST /issue-types/{id}/set-default + +Set an issue type as the default system type. + +**Path Parameters:** +- `id` (string, GUID) - Issue type ID + +**No Request Body** + +**Response:** +```json +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "tenantId": "SYSTEM", + "name": "Task", + "description": "General task or work item", + "color": "#4CAF50", + "icon": "task", + "order": 3, + "isActive": true, + "isSystemType": true, + "isDefault": true, + "createdAt": "2025-01-15T10:30:00Z", + "updatedAt": "2026-02-04T11:30:00Z" +} +``` + +**Status Codes:** +- `200 OK` - Default set successfully +- `400 Bad Request` - Invalid ID or not a system type +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Issue type not found + +**Notes:** +- Unsets previous default automatically +- Only one default allowed per tenant +- Must be active to set as default + +--- + +## Issue Status Configuration + +System-level issue statuses managed by admins. + +### GET /issue-statuses + +List all system issue status configurations. + +**Query Parameters:** +- `includeInactive` (boolean, optional): Include inactive statuses (default: false) + +**Response:** +```json +[ + { + "id": "770e8400-e29b-41d4-a716-446655440000", + "tenantId": null, + "name": "Open", + "category": "Open", + "order": 1, + "isSystemStatus": true, + "isActive": true, + "createdAt": "2025-01-15T10:30:00Z", + "updatedAt": "2025-01-20T14:45:00Z" + } +] +``` + +**Status Codes:** +- `200 OK` - Statuses retrieved successfully +- `400 Bad Request` - Invalid query parameters +- `401 Unauthorized` - Missing or invalid admin token + +--- + +### GET /issue-statuses/{id} + +Get a single issue status configuration by ID. + +**Path Parameters:** +- `id` (string, GUID) - Issue status ID + +**Response:** +```json +{ + "id": "770e8400-e29b-41d4-a716-446655440000", + "tenantId": null, + "name": "Open", + "category": "Open", + "order": 1, + "isSystemStatus": true, + "isActive": true, + "createdAt": "2025-01-15T10:30:00Z", + "updatedAt": "2025-01-20T14:45:00Z" +} +``` + +**Status Codes:** +- `200 OK` - Status retrieved successfully +- `400 Bad Request` - Invalid ID format +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Issue status not found or not a system status + +--- + +### POST /issue-statuses + +Create a new system issue status configuration. + +**Request:** +```json +{ + "name": "Needs Review", + "category": "InProgress", + "order": 5, + "description": "Waiting for review", + "color": "#FFA726", + "icon": "review" +} +``` + +**Validations:** +- `name`: Required, 1-50 characters, unique within system statuses +- `category`: Required, valid StatusCategory enum (Open, InProgress, Resolved, Closed) +- `order`: Required, positive integer +- `description`: Optional, max 200 characters +- `color`: Optional, valid hex color format +- `icon`: Optional, 1-30 characters + +**Response:** +```json +{ + "id": "880e8400-e29b-41d4-a716-446655440000", + "tenantId": null, + "name": "Needs Review", + "category": "InProgress", + "order": 5, + "isSystemStatus": true, + "isActive": true, + "createdAt": "2026-02-04T10:30:00Z", + "updatedAt": "2026-02-04T10:30:00Z" +} +``` + +**Status Codes:** +- `201 Created` - Issue status created successfully +- `400 Bad Request` - Invalid request data or duplicate name +- `401 Unauthorized` - Missing or invalid admin token + +--- + +### PUT /issue-statuses/{id} + +Update an existing system issue status configuration. + +**Path Parameters:** +- `id` (string, GUID) - Issue status ID + +**Request:** +```json +{ + "name": "Under Review", + "category": "InProgress", + "order": 4, + "description": "Currently under review", + "color": "#FF9800", + "icon": "magnify" +} +``` + +**Validations:** +- All fields optional (partial updates supported) +- Same validation rules as POST + +**Response:** +```json +{ + "id": "880e8400-e29b-41d4-a716-446655440000", + "tenantId": null, + "name": "Under Review", + "category": "InProgress", + "order": 4, + "isSystemStatus": true, + "isActive": true, + "createdAt": "2026-02-04T10:30:00Z", + "updatedAt": "2026-02-04T11:00:00Z" +} +``` + +**Status Codes:** +- `200 OK` - Issue status updated successfully +- `400 Bad Request` - Invalid request data or not a system status +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Issue status not found + +--- + +### POST /issue-statuses/{id}/deactivate + +Deactivate an issue status configuration (sets isActive=false). + +**Path Parameters:** +- `id` (string, GUID) - Issue status ID + +**No Request Body** + +**Response:** +```json +{ + "id": "880e8400-e29b-41d4-a716-446655440000", + "tenantId": null, + "name": "Under Review", + "category": "InProgress", + "order": 4, + "isSystemStatus": true, + "isActive": false, + "createdAt": "2026-02-04T10:30:00Z", + "updatedAt": "2026-02-04T11:15:00Z" +} +``` + +**Status Codes:** +- `200 OK` - Issue status deactivated successfully +- `400 Bad Request` - Invalid ID, not a system status, or already inactive +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Issue status not found + +--- + +### POST /issue-statuses/{id}/activate + +Activate (restore) a deactivated issue status. + +**Path Parameters:** +- `id` (string, GUID) - Issue status ID + +**No Request Body** + +**Response:** +```json +{ + "id": "880e8400-e29b-41d4-a716-446655440000", + "tenantId": null, + "name": "Under Review", + "category": "InProgress", + "order": 4, + "isSystemStatus": true, + "isActive": true, + "createdAt": "2026-02-04T10:30:00Z", + "updatedAt": "2026-02-04T11:30:00Z" +} +``` + +**Status Codes:** +- `200 OK` - Issue status activated successfully +- `400 Bad Request` - Invalid ID, not a system status, or already active +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Issue status not found + +--- + +## Email Template Management + +Comprehensive email template management system with RazorLight rendering engine. + +### GET /email-templates + +List all email templates with pagination and filtering. + +**Query Parameters:** +- `page` (integer, optional): Page number (default: 1) +- `pageSize` (integer, optional): Items per page (default: 20, max: 100) +- `category` (string, optional): Filter by category +- `isActive` (boolean, optional): Filter by active status + +**Response:** +```json +{ + "templates": [ + { + "id": "990e8400-e29b-41d4-a716-446655440000", + "name": "email_verification", + "subject": "Verify Your Email Address - PurposePath", + "description": "Default email verification template", + "category": "authentication", + "language": "en", + "isActive": true, + "isDefault": false, + "createdBy": "system", + "createdAt": "2025-01-15T10:30:00Z", + "updatedAt": "2025-01-20T14:45:00Z", + "version": 1 + } + ], + "totalCount": 10, + "currentPage": 1, + "pageSize": 20 +} +``` + +**Status Codes:** +- `200 OK` - Templates retrieved successfully +- `400 Bad Request` - Invalid pagination or filter parameters +- `401 Unauthorized` - Missing or invalid admin token + +--- + +### GET /email-templates/{id} + +Get a specific email template by ID. + +**Path Parameters:** +- `id` (string, GUID) - Template ID + +**Response:** +```json +{ + "id": "990e8400-e29b-41d4-a716-446655440000", + "name": "email_verification", + "subject": "Verify Your Email Address - PurposePath", + "description": "Default email verification template", + "category": "authentication", + "htmlContent": "...", + "textContent": "Welcome to PurposePath!...", + "variables": [ + { + "name": "FirstName", + "type": "String", + "description": "Recipient first name", + "required": true + }, + { + "name": "VerificationLink", + "type": "Url", + "description": "Email verification link", + "required": true + } + ], + "tags": ["verification", "onboarding"], + "language": "en", + "isActive": true, + "isDefault": false, + "isSystem": false, + "version": 1, + "createdBy": "system", + "createdAt": "2025-01-15T10:30:00Z", + "updatedAt": "2025-01-20T14:45:00Z" +} +``` + +**Status Codes:** +- `200 OK` - Template retrieved successfully +- `400 Bad Request` - Invalid ID format +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Template not found + +--- + +### POST /email-templates + +Create a new email template. + +**Request:** +```json +{ + "name": "custom_welcome", + "subject": "Welcome to Our Platform!", + "description": "Custom welcome email for new users", + "category": "onboarding", + "htmlContent": "...", + "textContent": "Welcome {{firstName}}!...", + "variables": [ + { + "name": "FirstName", + "type": "String", + "description": "User's first name", + "required": true + } + ], + "tags": ["welcome", "custom"], + "language": "en", + "isActive": true +} +``` + +**Validations:** +- `name`: Required, unique, 1-100 characters, lowercase with underscores +- `subject`: Required, 1-200 characters +- `description`: Optional, max 500 characters +- `category`: Required, valid category (authentication, onboarding, trial, payment, subscription) +- `htmlContent`: Required, valid HTML +- `textContent`: Optional, plain text version +- `variables`: Required array, each with name, type, description, required flag +- `language`: Optional, ISO 639-1 code (default: "en") + +**Response:** +```json +{ + "id": "aa0e8400-e29b-41d4-a716-446655440000", + "name": "custom_welcome", + "subject": "Welcome to Our Platform!", + "description": "Custom welcome email for new users", + "category": "onboarding", + "htmlContent": "...", + "textContent": "Welcome {{firstName}}!...", + "variables": [...], + "tags": ["welcome", "custom"], + "language": "en", + "isActive": true, + "isDefault": false, + "version": 1, + "createdBy": "admin-user-id", + "createdAt": "2026-02-04T10:30:00Z", + "updatedAt": "2026-02-04T10:30:00Z" +} +``` + +**Status Codes:** +- `201 Created` - Template created successfully +- `400 Bad Request` - Invalid request data or duplicate name +- `401 Unauthorized` - Missing or invalid admin token + +--- + +### PATCH /email-templates/{id} + +Update an existing email template (partial update). + +**Path Parameters:** +- `id` (string, GUID) - Template ID + +**Request:** +```json +{ + "subject": "Updated Subject Line", + "description": "Updated description", + "htmlContent": "...", + "isActive": true +} +``` + +**Validations:** +- All fields optional (partial update) +- Same validation rules as POST for provided fields + +**Response:** +```json +{ + "id": "aa0e8400-e29b-41d4-a716-446655440000", + "name": "custom_welcome", + "subject": "Updated Subject Line", + "description": "Updated description", + "category": "onboarding", + "htmlContent": "...", + "textContent": "Welcome {{firstName}}!...", + "variables": [...], + "tags": ["welcome", "custom"], + "language": "en", + "isActive": true, + "isDefault": false, + "version": 2, + "createdBy": "admin-user-id", + "createdAt": "2026-02-04T10:30:00Z", + "updatedAt": "2026-02-04T11:00:00Z" +} +``` + +**Status Codes:** +- `200 OK` - Template updated successfully +- `400 Bad Request` - Invalid request data +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Template not found + +--- + +### POST /email-templates/{id}/clone + +Clone an existing email template. + +**Path Parameters:** +- `id` (string, GUID) - Source template ID + +**Request:** +```json +{ + "newName": "custom_welcome_v2", + "newDescription": "Cloned welcome email template", + "newLanguage": "en" +} +``` + +**Response:** +```json +{ + "id": "bb0e8400-e29b-41d4-a716-446655440000", + "name": "custom_welcome_v2", + "subject": "Welcome to Our Platform!", + "description": "Cloned welcome email template", + "category": "onboarding", + "htmlContent": "...", + "textContent": "Welcome {{firstName}}!...", + "variables": [...], + "tags": ["welcome", "custom"], + "language": "en", + "isActive": false, + "isDefault": false, + "version": 1, + "createdBy": "admin-user-id", + "createdAt": "2026-02-04T11:00:00Z", + "updatedAt": "2026-02-04T11:00:00Z" +} +``` + +**Status Codes:** +- `201 Created` - Template cloned successfully +- `400 Bad Request` - Invalid source ID or duplicate new name +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Source template not found + +**Notes:** +- Cloned templates start as inactive (isActive=false) +- All content and variables copied from source +- New template gets new ID and creation timestamp + +--- + +### POST /email-templates/{id}/preview + +Preview rendered email template with sample data. + +**Path Parameters:** +- `id` (string, GUID) - Template ID + +**Request:** +```json +{ + "previewData": { + "FirstName": "John", + "VerificationLink": "https://app.purposepath.com/verify?token=abc123" + }, + "renderFormat": "html" +} +``` + +**Response:** +```json +{ + "renderedHtml": "...

Hi John,

...", + "renderedText": "Hi John,\n\nThank you for registering...", + "subject": "Verify Your Email Address - PurposePath", + "variables": [ + { + "name": "FirstName", + "value": "John", + "provided": true + }, + { + "name": "VerificationLink", + "value": "https://app.purposepath.com/verify?token=abc123", + "provided": true + } + ] +} +``` + +**Status Codes:** +- `200 OK` - Preview rendered successfully +- `400 Bad Request` - Invalid template ID or missing required variables +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Template not found + +--- + +### POST /email-templates/{id}/test + +Send test email using template. + +**Path Parameters:** +- `id` (string, GUID) - Template ID + +**Request:** +```json +{ + "testEmail": "admin@example.com", + "testData": { + "FirstName": "Test User", + "VerificationLink": "https://app.purposepath.com/verify?token=test123" + } +} +``` + +**Response:** +```json +{ + "success": true, + "messageId": "550e8400-e29b-41d4-a716-446655440000", + "sentTo": "admin@example.com", + "sentAt": "2026-02-04T11:30:00Z" +} +``` + +**Status Codes:** +- `200 OK` - Test email sent successfully +- `400 Bad Request` - Invalid request data or missing variables +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Template not found +- `500 Internal Server Error` - Email delivery failure + +--- + +### GET /email-templates/{id}/analytics + +Get template usage analytics. + +**Path Parameters:** +- `id` (string, GUID) - Template ID + +**Response:** +```json +{ + "templateId": "990e8400-e29b-41d4-a716-446655440000", + "templateName": "email_verification", + "totalSent": 15420, + "sentLast30Days": 1250, + "sentLast7Days": 310, + "sentToday": 45, + "averageOpenRate": 72.5, + "averageClickRate": 34.2, + "lastSent": "2026-02-04T10:00:00Z", + "mostRecentErrors": [ + { + "errorMessage": "Invalid recipient email", + "occurredAt": "2026-02-03T15:30:00Z", + "count": 2 + } + ] +} +``` + +**Status Codes:** +- `200 OK` - Analytics retrieved successfully +- `400 Bad Request` - Invalid template ID +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Template not found + +--- + +### DELETE /email-templates/{id} + +Delete an email template (soft delete). + +**Path Parameters:** +- `id` (string, GUID) - Template ID + +**No Request Body** + +**Response:** +``` +204 No Content +``` + +**Status Codes:** +- `204 No Content` - Template deleted successfully +- `400 Bad Request` - Invalid ID or system template (cannot delete) +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Template not found + +**Notes:** +- Soft delete only (sets isActive=false, retains data) +- System templates cannot be deleted +- Templates with active email sends retained for audit + +--- + +### GET /email-templates/categories + +Get list of available template categories. + +**No Query Parameters** + +**Response:** +```json +{ + "categories": [ + { + "category": "authentication", + "description": "Email verification and confirmation", + "templateCount": 3 + }, + { + "category": "onboarding", + "description": "Welcome and onboarding emails", + "templateCount": 1 + }, + { + "category": "trial", + "description": "Trial period notifications", + "templateCount": 3 + }, + { + "category": "payment", + "description": "Payment and billing notifications", + "templateCount": 3 + } + ] +} +``` + +**Status Codes:** +- `200 OK` - Categories retrieved successfully +- `401 Unauthorized` - Missing or invalid admin token + +--- + +## Subscriber Management + +Comprehensive tenant/subscriber management for viewing and managing customer subscriptions. + +### GET /subscribers + +Get paginated list of subscribers with filtering and sorting. + +**Query Parameters:** +- `page` (integer, optional): Page number (default: 1) +- `pageSize` (integer, optional): Items per page (default: 50, max: 100) +- `search` (string, optional): Search by tenant name or email +- `status` (string, optional): Filter by subscription status (Active, Trial, Cancelled, Suspended) +- `tier` (string, GUID, optional): Filter by subscription tier ID +- `renewalFrequency` (string, optional): Filter by billing frequency (Monthly, Yearly) +- `sortBy` (string, optional): Sort field (Name, CreatedAt, Status, TierId, Frequency) +- `sortOrder` (string, optional): Sort direction (Ascending, Descending) + +**Response:** +```json +{ + "items": [ + { + "tenantId": "cc0e8400-e29b-41d4-a716-446655440000", + "businessName": "Acme Corporation", + "ownerEmail": "owner@acme.com", + "ownerName": "John Doe", + "subscription": { + "id": "dd0e8400-e29b-41d4-a716-446655440000", + "status": "Active", + "tier": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "name": "Professional", + "displayName": "Professional" + }, + "frequency": "Monthly", + "startDate": "2025-01-15T00:00:00Z", + "currentPeriodEnd": "2026-03-15T00:00:00Z", + "autoRenew": true, + "monthlyPrice": 29.99, + "yearlyPrice": 299.99 + }, + "userCount": 5, + "createdAt": "2025-01-15T10:30:00Z", + "lastActivityAt": "2026-02-04T09:15:00Z" + } + ], + "pagination": { + "currentPage": 1, + "pageSize": 50, + "totalCount": 342, + "totalPages": 7 + } +} +``` + +**Status Codes:** +- `200 OK` - Subscribers retrieved successfully +- `400 Bad Request` - Invalid query parameters +- `401 Unauthorized` - Missing or invalid admin token + +**Notes:** +- Search performs case-insensitive match on business name and owner email +- All filters can be combined +- Results sorted by specified field and order +- Audit log entry created for admin action + +--- + +### GET /subscribers/{tenantId} + +Get detailed information about a specific subscriber. + +**Path Parameters:** +- `tenantId` (string, GUID) - Tenant ID + +**Response:** +```json +{ + "tenantId": "cc0e8400-e29b-41d4-a716-446655440000", + "businessName": "Acme Corporation", + "website": "https://acme.com", + "industry": "Technology", + "address": { + "street": "123 Main St", + "city": "San Francisco", + "state": "CA", + "postalCode": "94105", + "country": "USA" + }, + "owner": { + "userId": "ee0e8400-e29b-41d4-a716-446655440000", + "email": "owner@acme.com", + "firstName": "John", + "lastName": "Doe", + "createdAt": "2025-01-15T10:30:00Z" + }, + "subscription": { + "id": "dd0e8400-e29b-41d4-a716-446655440000", + "status": "Active", + "tier": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "name": "Professional", + "displayName": "Professional", + "features": ["Goals", "Operations", "Measures", "Strategies", "Realtime"], + "limits": { + "goals": 25, + "actions": 150, + "strategies": 15 + } + }, + "frequency": "Monthly", + "startDate": "2025-01-15T00:00:00Z", + "currentPeriodStart": "2026-02-15T00:00:00Z", + "currentPeriodEnd": "2026-03-15T00:00:00Z", + "trialEnd": null, + "autoRenew": true, + "cancelAt": null, + "cancelledAt": null, + "monthlyPrice": 29.99, + "yearlyPrice": 299.99, + "currency": "USD" + }, + "usage": { + "userCount": 5, + "goalCount": 12, + "actionCount": 87, + "measureCount": 24 + }, + "featureGrants": [ + { + "featureCode": "BulkPlanner", + "grantedAt": "2025-06-01T00:00:00Z", + "expiresAt": "2026-06-01T00:00:00Z", + "reason": "Beta tester access" + } + ], + "paymentMethod": { + "type": "card", + "last4": "4242", + "brand": "Visa", + "expiryMonth": 12, + "expiryYear": 2027 + }, + "createdAt": "2025-01-15T10:30:00Z", + "updatedAt": "2026-02-04T09:15:00Z", + "lastActivityAt": "2026-02-04T09:15:00Z" +} +``` + +**Status Codes:** +- `200 OK` - Subscriber details retrieved successfully +- `400 Bad Request` - Invalid tenant ID format +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Tenant not found + +**Notes:** +- Provides comprehensive view of tenant, subscription, usage, and payment details +- Includes granted features beyond tier +- Shows current billing period and renewal status +- Audit log entry created + +--- + +## Plan Management + +Subscription tier/plan management including creation, updates, and deactivation. + +### GET /plans + +Get paginated list of subscription plans. + +**Query Parameters:** +- `page` (integer, optional): Page number (default: 1) +- `pageSize` (integer, optional): Items per page (default: 20, max: 100) +- `isActive` (boolean, optional): Filter by active status +- `includeUsage` (boolean, optional): Include subscriber counts (default: false) + +**Response:** +```json +{ + "items": [ + { + "id": "550e8400-e29b-41d4-a716-446655440001", + "name": "Professional", + "displayName": "Professional", + "description": "Designed for growing teams...", + "monthlyPrice": 29.99, + "yearlyPrice": 299.99, + "currency": "USD", + "isActive": true, + "sortOrder": 2, + "subscriberCount": 142, + "createdAt": "2025-01-01T00:00:00Z", + "updatedAt": "2025-12-15T10:00:00Z" + } + ], + "pagination": { + "currentPage": 1, + "pageSize": 20, + "totalCount": 3, + "totalPages": 1 + } +} +``` + +**Status Codes:** +- `200 OK` - Plans retrieved successfully +- `400 Bad Request` - Invalid query parameters +- `401 Unauthorized` - Missing or invalid admin token + +--- + +### GET /plans/{id} + +Get detailed information about a specific plan. + +**Path Parameters:** +- `id` (string, GUID) - Plan ID + +**Query Parameters:** +- `includeUsage` (boolean, optional): Include detailed usage statistics (default: false) + +**Response:** +```json +{ + "id": "550e8400-e29b-41d4-a716-446655440001", + "name": "Professional", + "displayName": "Professional", + "description": "Designed for growing teams that need advanced features...", + "pricing": { + "monthlyPrice": 29.99, + "yearlyPrice": 299.99, + "currency": "USD" + }, + "features": [ + "Goals", + "Operations", + "Measures", + "Strategies", + "Realtime", + "Reports", + "Attachments", + "BulkPlanner", + "StrategyCompare" + ], + "limits": { + "goals": 25, + "actions": 150, + "strategies": 15, + "measures": 50, + "attachments": 250, + "reports": 25 + }, + "supportedFrequencies": ["Monthly", "Yearly"], + "isActive": true, + "sortOrder": 2, + "usage": { + "totalSubscribers": 142, + "activeSubscribers": 138, + "trialSubscribers": 4, + "monthlyRevenue": 4137.62, + "yearlyRevenue": 0, + "totalRevenue": 4137.62 + }, + "createdAt": "2025-01-01T00:00:00Z", + "updatedAt": "2025-12-15T10:00:00Z" +} +``` + +**Status Codes:** +- `200 OK` - Plan details retrieved successfully +- `400 Bad Request` - Invalid plan ID format +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Plan not found + +--- + +### POST /plans + +Create a new subscription plan. + +**Request:** +```json +{ + "name": "Enterprise", + "displayName": "Enterprise", + "description": "Unlimited power for large organizations...", + "pricing": { + "monthlyPrice": 99.99, + "yearlyPrice": 999.99, + "currency": "USD" + }, + "features": [ + "Goals", + "Operations", + "Measures", + "Strategies", + "Realtime", + "Reports", + "Attachments", + "BulkPlanner", + "StrategyCompare", + "GoalCreate" + ], + "limits": { + "goals": null, + "actions": null, + "strategies": null, + "measures": null, + "attachments": null, + "reports": null + }, + "supportedFrequencies": ["Monthly", "Yearly"], + "isActive": true, + "sortOrder": 3 +} +``` + +**Validations:** +- `name`: Required, unique, 1-50 characters, PascalCase +- `displayName`: Required, 1-100 characters +- `description`: Required, 1-500 characters +- `pricing.monthlyPrice`: Required, positive decimal +- `pricing.yearlyPrice`: Required, positive decimal +- `pricing.currency`: Required, 3-letter ISO currency code +- `features`: Required array of valid FeatureName enums +- `limits`: Object with integer values or null (unlimited) +- `supportedFrequencies`: Required array, at least one frequency +- `sortOrder`: Required, positive integer + +**Response:** +```json +{ + "id": "550e8400-e29b-41d4-a716-446655440002", + "name": "Enterprise", + "displayName": "Enterprise", + "description": "Unlimited power for large organizations...", + "pricing": { + "monthlyPrice": 99.99, + "yearlyPrice": 999.99, + "currency": "USD" + }, + "features": [...], + "limits": {...}, + "supportedFrequencies": ["Monthly", "Yearly"], + "isActive": true, + "sortOrder": 3, + "createdAt": "2026-02-04T10:30:00Z", + "updatedAt": "2026-02-04T10:30:00Z" +} +``` + +**Status Codes:** +- `201 Created` - Plan created successfully +- `400 Bad Request` - Invalid request data +- `401 Unauthorized` - Missing or invalid admin token +- `409 Conflict` - Plan name already exists + +**Notes:** +- Creates new plan immediately available for subscriptions +- Audit log entry created +- Admin user ID tracked in metadata + +--- + +### PUT /plans/{id} + +Update an existing subscription plan. + +**Path Parameters:** +- `id` (string, GUID) - Plan ID + +**Request:** +```json +{ + "displayName": "Professional Plus", + "description": "Enhanced professional tier...", + "pricing": { + "monthlyPrice": 34.99, + "yearlyPrice": 349.99, + "currency": "USD" + }, + "features": [...], + "limits": {...}, + "supportedFrequencies": ["Monthly", "Yearly"], + "isActive": true, + "sortOrder": 2 +} +``` + +**Validations:** +- All fields optional (partial update) +- Same validation rules as POST for provided fields +- `name` cannot be changed (immutable) + +**Response:** +```json +{ + "id": "550e8400-e29b-41d4-a716-446655440001", + "name": "Professional", + "displayName": "Professional Plus", + "description": "Enhanced professional tier...", + "pricing": { + "monthlyPrice": 34.99, + "yearlyPrice": 349.99, + "currency": "USD" + }, + "features": [...], + "limits": {...}, + "supportedFrequencies": ["Monthly", "Yearly"], + "isActive": true, + "sortOrder": 2, + "createdAt": "2025-01-01T00:00:00Z", + "updatedAt": "2026-02-04T11:00:00Z" +} +``` + +**Status Codes:** +- `200 OK` - Plan updated successfully +- `400 Bad Request` - Invalid request data +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Plan not found + +**Notes:** +- Existing subscriptions retain current pricing +- New subscriptions use updated pricing +- Feature/limit changes require validation +- Breaking changes flagged in audit log + +--- + +### POST /plans/{id}/deactivate + +Deactivate a subscription plan with grandfathering or migration options. + +**Path Parameters:** +- `id` (string, GUID) - Plan ID + +**Request:** +```json +{ + "grandfatherExisting": true, + "migrationPlanId": "550e8400-e29b-41d4-a716-446655440002", + "effectiveDate": "2026-03-01T00:00:00Z", + "reason": "Plan consolidation - migrating to Enterprise tier" +} +``` + +**Validations:** +- `grandfatherExisting`: Required, boolean +- `migrationPlanId`: Optional GUID (required if grandfatherExisting=false) +- `effectiveDate`: Optional ISO 8601 date (default: immediate) +- `reason`: Required, 1-500 characters + +**Response:** +```json +{ + "planId": "550e8400-e29b-41d4-a716-446655440001", + "planName": "Professional", + "deactivated": true, + "effectiveDate": "2026-03-01T00:00:00Z", + "grandfatheredSubscribers": 142, + "migrationPlanId": "550e8400-e29b-41d4-a716-446655440002", + "migratedSubscribers": 0, + "reason": "Plan consolidation - migrating to Enterprise tier", + "deactivatedAt": "2026-02-04T11:00:00Z", + "deactivatedBy": "admin-user-id" +} +``` + +**Status Codes:** +- `200 OK` - Plan deactivated successfully +- `400 Bad Request` - Invalid request data +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Plan not found + +**Notes:** +- If `grandfatherExisting=true`: Existing subscribers keep plan +- If `grandfatherExisting=false`: Requires migration plan for active subscribers +- Deactivated plans hidden from new subscriptions +- Audit log entry tracks reason and affected subscribers + +--- + +### DELETE /plans/{id} + +Delete a subscription plan (with safety checks). + +**Path Parameters:** +- `id` (string, GUID) - Plan ID + +**No Request Body** + +**Response:** +``` +204 No Content +``` + +**Status Codes:** +- `204 No Content` - Plan deleted successfully +- `400 Bad Request` - Invalid plan ID +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Plan not found +- `409 Conflict` - Plan has active subscriptions (cannot delete) + +**Restrictions:** +- Can ONLY delete plans with zero active subscriptions +- Plans with subscription history must be deactivated instead +- System default plans cannot be deleted + +**Notes:** +- Hard delete (permanent removal) +- Use POST /plans/{id}/deactivate for plans with subscribers +- Audit log entry created before deletion + +--- + +### POST /plans/{id}/validate + +Validate plan updates before applying to check for breaking changes. + +**Path Parameters:** +- `id` (string, GUID) - Plan ID + +**Request:** +```json +{ + "displayName": "Professional Plus", + "pricing": { + "monthlyPrice": 34.99, + "yearlyPrice": 349.99, + "currency": "USD" + }, + "features": [...], + "limits": {...} +} +``` + +**Response:** +```json +{ + "isValid": true, + "warnings": [], + "breakingChanges": [ + { + "type": "FeatureRemoval", + "description": "Removing feature 'Realtime' will affect 45 subscribers", + "affectedCount": 45, + "severity": "High" + }, + { + "type": "LimitReduction", + "description": "Reducing goal limit from 25 to 15 will affect 12 subscribers exceeding new limit", + "affectedCount": 12, + "severity": "Medium" + } + ], + "nonBreakingChanges": [ + { + "type": "PriceIncrease", + "description": "Monthly price increasing from $29.99 to $34.99", + "affectedCount": 142, + "severity": "Low" + } + ], + "recommendations": [ + "Consider grandfathering existing subscribers on current pricing", + "Notify affected subscribers 30 days before feature removal", + "Provide migration path for subscribers exceeding new limits" + ] +} +``` + +**Status Codes:** +- `200 OK` - Validation completed +- `400 Bad Request` - Invalid request data +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Plan not found + +**Notes:** +- Non-destructive validation only +- Checks for breaking changes and affected subscribers +- Provides recommendations for safe rollout +- Use before applying changes to production plans + +--- + +### GET /plans/{id}/affected-subscribers + +Get count of subscribers affected by a plan. + +**Path Parameters:** +- `id` (string, GUID) - Plan ID + +**Response:** +```json +{ + "planId": "550e8400-e29b-41d4-a716-446655440001", + "planName": "Professional", + "totalSubscribers": 142, + "activeSubscribers": 138, + "trialSubscribers": 4, + "cancelledSubscribers": 0, + "suspendedSubscribers": 0, + "subscribersByFrequency": { + "Monthly": 120, + "Yearly": 22 + }, + "revenueImpact": { + "monthlyRevenue": 4137.62, + "yearlyRevenue": 0, + "projectedAnnualRevenue": 49651.44 + } +} +``` + +**Status Codes:** +- `200 OK` - Affected subscribers retrieved successfully +- `400 Bad Request` - Invalid plan ID +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Plan not found + +--- + +## Feature Management + +Manage system features and tenant-specific feature grants. + +### GET /features + +Get list of all available system features. + +**No Query Parameters** + +**Response:** +```json +{ + "features": [ + { + "code": "Goals", + "name": "Goal Management", + "description": "Create and track strategic goals", + "category": "Core" + }, + { + "code": "Operations", + "name": "Operations Management", + "description": "Manage actions and issues", + "category": "Core" + }, + { + "code": "Measures", + "name": "MEASURE Management", + "description": "Track and analyze MEASUREs", + "category": "Core" + }, + { + "code": "Realtime", + "name": "Real-time Collaboration", + "description": "Real-time updates via SSE", + "category": "Advanced" + }, + { + "code": "BulkPlanner", + "name": "Bulk Planning", + "description": "Bulk create actions and strategies", + "category": "Premium" + } + ] +} +``` + +**Status Codes:** +- `200 OK` - Features retrieved successfully +- `401 Unauthorized` - Missing or invalid admin token + +--- + +### GET /features/tiers + +Get feature matrix for all subscription tiers. + +**No Query Parameters** + +**Response:** +```json +{ + "tiers": [ + { + "tierId": "550e8400-e29b-41d4-a716-446655440000", + "tierName": "Basic", + "features": ["Goals", "Operations", "Measures"] + }, + { + "tierId": "550e8400-e29b-41d4-a716-446655440001", + "tierName": "Professional", + "features": [ + "Goals", + "Operations", + "Measures", + "Strategies", + "Realtime", + "Reports", + "Attachments", + "BulkPlanner", + "StrategyCompare" + ] + }, + { + "tierId": "550e8400-e29b-41d4-a716-446655440002", + "tierName": "Enterprise", + "features": [ + "Goals", + "Operations", + "Measures", + "Strategies", + "Realtime", + "Reports", + "Attachments", + "BulkPlanner", + "StrategyCompare", + "GoalCreate" + ] + } + ] +} +``` + +**Status Codes:** +- `200 OK` - Tier features retrieved successfully +- `401 Unauthorized` - Missing or invalid admin token + +--- + +### PUT /features/tiers/{tierId} + +Update features for a subscription tier. + +**Path Parameters:** +- `tierId` (string, GUID) - Tier ID + +**Request:** +```json +{ + "features": [ + "Goals", + "Operations", + "Measures", + "Strategies", + "Realtime", + "Reports" + ], + "reason": "Removing premium features for tier consolidation" +} +``` + +**Validations:** +- `features`: Required array of valid FeatureName enums +- `reason`: Required, 1-500 characters (audit trail) + +**Response:** +```json +{ + "tierId": "550e8400-e29b-41d4-a716-446655440001", + "tierName": "Professional", + "features": [ + "Goals", + "Operations", + "Measures", + "Strategies", + "Realtime", + "Reports" + ], + "affectedSubscribers": 142, + "removedFeatures": ["BulkPlanner", "StrategyCompare"], + "addedFeatures": [], + "updatedAt": "2026-02-04T11:00:00Z", + "updatedBy": "admin-user-id", + "reason": "Removing premium features for tier consolidation" +} +``` + +**Status Codes:** +- `200 OK` - Tier features updated successfully +- `400 Bad Request` - Invalid request data or invalid feature codes +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Tier not found + +**Notes:** +- Changes apply to all subscribers immediately +- Removed features disabled for all tenants on tier +- Added features enabled for all tenants on tier +- Audit log entry tracks changes and reason +- Breaking changes require admin confirmation + +--- + +### POST /features/tiers/{tierId}/validate + +Validate tier feature changes before applying. + +**Path Parameters:** +- `tierId` (string, GUID) - Tier ID + +**Request:** +```json +{ + "features": [ + "Goals", + "Operations", + "Measures", + "Strategies", + "Realtime", + "Reports" + ] +} +``` + +**Response:** +```json +{ + "isValid": true, + "warnings": [], + "breakingChanges": [ + { + "type": "FeatureRemoval", + "feature": "BulkPlanner", + "affectedSubscribers": 45, + "description": "45 subscribers currently using Bulk Planning feature" + }, + { + "type": "FeatureRemoval", + "feature": "StrategyCompare", + "affectedSubscribers": 28, + "description": "28 subscribers currently using Strategy Comparison feature" + } + ], + "addedFeatures": [], + "removedFeatures": ["BulkPlanner", "StrategyCompare"], + "affectedSubscribers": 142, + "recommendations": [ + "Notify affected subscribers 30 days before feature removal", + "Consider offering feature grants to power users", + "Provide alternative workflows for removed features" + ] +} +``` + +**Status Codes:** +- `200 OK` - Validation completed +- `400 Bad Request` - Invalid request data +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Tier not found + +--- + +### GET /features/tenants/{tenantId}/grants + +Get tenant-specific feature grants. + +**Path Parameters:** +- `tenantId` (string, GUID) - Tenant ID + +**Response:** +```json +{ + "tenantId": "cc0e8400-e29b-41d4-a716-446655440000", + "businessName": "Acme Corporation", + "tierFeatures": ["Goals", "Operations", "Measures"], + "grants": [ + { + "grantId": "ff0e8400-e29b-41d4-a716-446655440000", + "featureCode": "BulkPlanner", + "featureName": "Bulk Planning", + "grantedAt": "2025-06-01T00:00:00Z", + "expiresAt": "2026-06-01T00:00:00Z", + "grantedBy": "admin-user-id", + "reason": "Beta tester access", + "isActive": true, + "isExpired": false + } + ], + "totalGrants": 1, + "activeGrants": 1, + "expiredGrants": 0 +} +``` + +**Status Codes:** +- `200 OK` - Feature grants retrieved successfully +- `400 Bad Request` - Invalid tenant ID +- `401 Unauthorized` - Missing or invalid admin token + +--- + +### POST /features/tenants/{tenantId}/grants + +Add a tenant-specific feature grant. + +**Path Parameters:** +- `tenantId` (string, GUID) - Tenant ID + +**Request:** +```json +{ + "feature": "BulkPlanner", + "expiresWithPlan": false, + "customExpirationDate": "2026-12-31T23:59:59Z", + "reason": "Promotional access for annual contract" +} +``` + +**Validations:** +- `feature`: Required, valid FeatureName enum +- `expiresWithPlan`: Required, boolean (if true, grant expires with subscription) +- `customExpirationDate`: Optional ISO 8601 date (required if expiresWithPlan=false) +- `reason`: Required, 1-500 characters + +**Response:** +```json +{ + "tenantId": "cc0e8400-e29b-41d4-a716-446655440000", + "businessName": "Acme Corporation", + "tierFeatures": ["Goals", "Operations", "Measures"], + "grants": [ + { + "grantId": "ff0e8400-e29b-41d4-a716-446655440001", + "featureCode": "BulkPlanner", + "featureName": "Bulk Planning", + "grantedAt": "2026-02-04T11:00:00Z", + "expiresAt": "2026-12-31T23:59:59Z", + "grantedBy": "admin-user-id", + "reason": "Promotional access for annual contract", + "isActive": true, + "isExpired": false + } + ], + "totalGrants": 1, + "activeGrants": 1, + "expiredGrants": 0 +} +``` + +**Status Codes:** +- `201 Created` - Feature grant added successfully +- `400 Bad Request` - Invalid request data +- `401 Unauthorized` - Missing or invalid admin token +- `409 Conflict` - Feature already granted or included in tier + +**Notes:** +- Grants override tier restrictions +- Expiration handled automatically +- Audit log entry created +- Feature enabled immediately for tenant + +--- + +### DELETE /features/tenants/{tenantId}/grants/{feature} + +Remove a tenant-specific feature grant. + +**Path Parameters:** +- `tenantId` (string, GUID) - Tenant ID +- `feature` (string) - Feature code + +**Request:** +```json +{ + "reason": "End of promotional period" +} +``` + +**Validations:** +- `reason`: Required, 1-500 characters + +**Response:** +``` +204 No Content +``` + +**Status Codes:** +- `204 No Content` - Feature grant removed successfully +- `400 Bad Request` - Invalid tenant ID or feature code +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - No active grant found for feature + +**Notes:** +- Immediately revokes feature access +- Audit log entry created +- Cannot remove tier-included features + +--- + +### GET /features/tenants/{tenantId}/effective + +Get effective features for a tenant (tier + grants). + +**Path Parameters:** +- `tenantId` (string, GUID) - Tenant ID + +**Response:** +```json +{ + "tenantId": "cc0e8400-e29b-41d4-a716-446655440000", + "businessName": "Acme Corporation", + "tier": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "Basic", + "features": ["Goals", "Operations", "Measures"] + }, + "grants": [ + { + "featureCode": "BulkPlanner", + "source": "Grant", + "expiresAt": "2026-06-01T00:00:00Z" + } + ], + "effectiveFeatures": [ + { + "code": "Goals", + "source": "Tier" + }, + { + "code": "Operations", + "source": "Tier" + }, + { + "code": "Measures", + "source": "Tier" + }, + { + "code": "BulkPlanner", + "source": "Grant", + "expiresAt": "2026-06-01T00:00:00Z" + } + ], + "totalFeatures": 4 +} +``` + +**Status Codes:** +- `200 OK` - Effective features retrieved successfully +- `400 Bad Request` - Invalid tenant ID +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Tenant not found + +--- + +## Subscription Operations + +Administrative subscription management operations (trials, discounts, billing extensions). + +### POST /subscriptions/{tenantId}/extend-trial + +Extend subscription trial period. + +**Path Parameters:** +- `tenantId` (string, GUID) - Tenant ID + +**Request:** +```json +{ + "newExpirationDate": "2026-03-15T23:59:59Z", + "reason": "Customer requested additional time for evaluation" +} +``` + +**Validations:** +- `newExpirationDate`: Required, ISO 8601 date, future date, after current trial end +- `reason`: Required, 1-500 characters + +**Response:** +```json +{ + "subscriptionId": "dd0e8400-e29b-41d4-a716-446655440000", + "tenantId": "cc0e8400-e29b-41d4-a716-446655440000", + "previousTrialEnd": "2026-02-15T23:59:59Z", + "newTrialEnd": "2026-03-15T23:59:59Z", + "daysExtended": 28, + "reason": "Customer requested additional time for evaluation", + "extendedAt": "2026-02-04T11:00:00Z", + "extendedBy": "admin-user-id" +} +``` + +**Status Codes:** +- `200 OK` - Trial extended successfully +- `400 Bad Request` - Invalid request data or subscription not in trial +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Tenant or subscription not found + +**Notes:** +- Only applies to subscriptions in trial status +- Extends trial period without changing billing +- Audit log entry created +- Email notification sent to tenant owner + +--- + +### POST /subscriptions/{tenantId}/apply-discount + +Apply ad-hoc discount to subscription. + +**Path Parameters:** +- `tenantId` (string, GUID) - Tenant ID + +**Request:** +```json +{ + "discountType": "Percentage", + "value": 25, + "cyclesToApply": 3, + "reason": "Customer service recovery - service outage compensation" +} +``` + +**Validations:** +- `discountType`: Required, enum (Percentage, FixedAmount) +- `value`: Required, positive decimal + - Percentage: 1-100 + - FixedAmount: positive amount in subscription currency +- `cyclesToApply`: Required, positive integer (number of billing cycles) +- `reason`: Required, 1-500 characters + +**Response:** +```json +{ + "subscriptionId": "dd0e8400-e29b-41d4-a716-446655440000", + "tenantId": "cc0e8400-e29b-41d4-a716-446655440000", + "discountType": "Percentage", + "value": 25, + "cyclesToApply": 3, + "currentPrice": 29.99, + "discountedPrice": 22.49, + "totalSavings": 22.50, + "startsAt": "2026-02-15T00:00:00Z", + "endsAt": "2026-05-15T00:00:00Z", + "reason": "Customer service recovery - service outage compensation", + "appliedAt": "2026-02-04T11:00:00Z", + "appliedBy": "admin-user-id" +} +``` + +**Status Codes:** +- `200 OK` - Discount applied successfully +- `400 Bad Request` - Invalid request data or subscription not active +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Tenant or subscription not found + +**Notes:** +- Applies to next N billing cycles +- Automatically removes after specified cycles +- Audit log entry created +- Email confirmation sent to tenant + +--- + +### POST /subscriptions/{tenantId}/extend-billing + +Extend billing period (skip payment cycles as credit). + +**Path Parameters:** +- `tenantId` (string, GUID) - Tenant ID + +**Request:** +```json +{ + "monthsToExtend": 3, + "reason": "Compensation for platform issues during Q1" +} +``` + +**Validations:** +- `monthsToExtend`: Required, positive integer (1-12) +- `reason`: Required, 1-500 characters + +**Response:** +```json +{ + "subscriptionId": "dd0e8400-e29b-41d4-a716-446655440000", + "tenantId": "cc0e8400-e29b-41d4-a716-446655440000", + "monthsExtended": 3, + "previousPeriodEnd": "2026-03-15T00:00:00Z", + "newPeriodEnd": "2026-06-15T00:00:00Z", + "creditValue": 89.97, + "reason": "Compensation for platform issues during Q1", + "extendedAt": "2026-02-04T11:00:00Z", + "extendedBy": "admin-user-id" +} +``` + +**Status Codes:** +- `200 OK` - Billing extended successfully +- `400 Bad Request` - Invalid request data or subscription not active +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Tenant or subscription not found + +**Notes:** +- Extends subscription without payment +- Credit applied as billing period extension +- Maintains same subscription tier and features +- Audit log entry created +- Email notification sent to tenant + +--- + +### POST /subscriptions/{tenantId}/grant-feature + +Grant feature to subscription (uses TenantFeatureGrant). + +**Path Parameters:** +- `tenantId` (string, GUID) - Tenant ID + +**Request:** +```json +{ + "feature": "BulkPlanner", + "expiresAt": "2026-06-01T00:00:00Z", + "reason": "Beta testing program participant" +} +``` + +**Validations:** +- `feature`: Required, valid FeatureName enum +- `expiresAt`: Optional ISO 8601 date (null for permanent) +- `reason`: Required, 1-500 characters + +**Response:** +```json +{ + "tenantId": "cc0e8400-e29b-41d4-a716-446655440000", + "featureCode": "BulkPlanner", + "featureName": "Bulk Planning", + "grantedAt": "2026-02-04T11:00:00Z", + "expiresAt": "2026-06-01T00:00:00Z", + "reason": "Beta testing program participant", + "grantedBy": "admin-user-id" +} +``` + +**Status Codes:** +- `200 OK` - Feature granted successfully +- `400 Bad Request` - Invalid request data or feature already granted +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Tenant not found + +**Notes:** +- Same as POST /features/tenants/{tenantId}/grants +- Provided for convenience in subscription context +- See Feature Management section for details + +--- + +### POST /subscriptions/{tenantId}/designate-test + +Designate subscription as test account. + +**Path Parameters:** +- `tenantId` (string, GUID) - Tenant ID + +**Request:** +```json +{ + "isTest": true, + "reason": "Internal testing environment" +} +``` + +**Validations:** +- `isTest`: Required, boolean +- `reason`: Required, 1-500 characters + +**Response:** +```json +{ + "subscriptionId": "dd0e8400-e29b-41d4-a716-446655440000", + "tenantId": "cc0e8400-e29b-41d4-a716-446655440000", + "isTest": true, + "previousStatus": false, + "reason": "Internal testing environment", + "updatedAt": "2026-02-04T11:00:00Z", + "updatedBy": "admin-user-id" +} +``` + +**Status Codes:** +- `200 OK` - Test status updated successfully +- `400 Bad Request` - Invalid request data +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Tenant or subscription not found + +**Notes:** +- Test accounts excluded from billing and analytics +- Flagged in reports and dashboards +- Can be toggled on/off +- Audit log entry created + +--- + +### GET /subscriptions/{tenantId}/audit-log + +Get subscription audit log. + +**Path Parameters:** +- `tenantId` (string, GUID) - Tenant ID + +**Query Parameters:** +- `page` (integer, optional): Page number (default: 1) +- `pageSize` (integer, optional): Items per page (default: 50, max: 100) +- `actionType` (string, optional): Filter by action type +- `from` (datetime, optional): Start date filter (ISO 8601) +- `to` (datetime, optional): End date filter (ISO 8601) + +**Response:** +```json +{ + "entries": [ + { + "id": "audit-550e8400-e29b-41d4-a716-446655440000", + "tenantId": "cc0e8400-e29b-41d4-a716-446655440000", + "actionType": "ExtendTrial", + "description": "Extended trial period from 2026-02-15 to 2026-03-15", + "performedBy": "admin-user-id", + "performedByName": "Admin User", + "reason": "Customer requested additional time for evaluation", + "metadata": { + "previousTrialEnd": "2026-02-15T23:59:59Z", + "newTrialEnd": "2026-03-15T23:59:59Z", + "daysExtended": 28 + }, + "performedAt": "2026-02-04T11:00:00Z" + } + ], + "pagination": { + "currentPage": 1, + "pageSize": 50, + "totalCount": 127, + "totalPages": 3 + } +} +``` + +**Status Codes:** +- `200 OK` - Audit log retrieved successfully +- `400 Bad Request` - Invalid query parameters +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Tenant not found + +**Notes:** +- Complete audit trail of all admin actions +- Filterable by action type and date range +- Includes admin user details and reasons +- Metadata contains action-specific details + +--- + +## Discount Code Management + +Manage promotional discount codes for subscriptions. Discount codes can be percentage or fixed amount discounts, applicable to specific tiers, tenants, or system-wide. + +### GET /discount-codes + +Get paginated list of discount codes with optional filtering. + +**Query Parameters:** +- `page` (integer, optional) - Page number (default: 1) +- `pageSize` (integer, optional) - Items per page (default: 50, max: 100) +- `search` (string, optional) - Search by code name or description +- `status` ('active' | 'inactive' | 'all', optional) - Filter by status (default: 'all') +- `discountType` ('percentage' | 'fixed_amount' | 'all', optional) - Filter by discount type (default: 'all') +- `applicability` ('new_tenants' | 'renewals' | 'all', optional) - Filter by applicability (default: 'all') + +**Response:** +```json +{ + "items": [ + { + "id": "aa0e8400-e29b-41d4-a716-446655440000", + "codeName": "WELCOME20", + "description": "20% discount for new customers", + "discountType": "percentage", + "discountValue": 20, + "applicability": "new_tenants", + "applicableTiers": ["tier-id-1", "tier-id-2"], + "isSystemWide": true, + "tenantRestrictions": [], + "expiresAt": "2026-12-31T23:59:59Z", + "usageLimit": 100, + "currentUsage": 45, + "isActive": true, + "createdAt": "2026-01-01T00:00:00Z", + "createdBy": "admin-user-id", + "updatedAt": "2026-02-01T10:00:00Z" + } + ], + "pagination": { + "currentPage": 1, + "pageSize": 50, + "totalCount": 12, + "totalPages": 1 + } +} +``` + +**Status Codes:** +- `200 OK` - Discount codes retrieved successfully +- `400 Bad Request` - Invalid query parameters +- `401 Unauthorized` - Missing or invalid admin token + +--- + +### GET /discount-codes/{id} + +Get details of a specific discount code. + +**Path Parameters:** +- `id` (string, GUID) - Discount code ID + +**Response:** +```json +{ + "success": true, + "data": { + "id": "aa0e8400-e29b-41d4-a716-446655440000", + "codeName": "WELCOME20", + "description": "20% discount for new customers", + "discountType": "percentage", + "discountValue": 20, + "applicability": "new_tenants", + "applicableTiers": ["tier-id-1", "tier-id-2"], + "isSystemWide": true, + "tenantRestrictions": [], + "expiresAt": "2026-12-31T23:59:59Z", + "usageLimit": 100, + "currentUsage": 45, + "isActive": true, + "createdAt": "2026-01-01T00:00:00Z", + "createdBy": "admin-user-id", + "updatedAt": "2026-02-01T10:00:00Z" + } +} +``` + +**Status Codes:** +- `200 OK` - Discount code retrieved successfully +- `400 Bad Request` - Invalid discount code ID +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Discount code not found + +--- + +### GET /discount-codes/{id}/usage + +Get usage history for a specific discount code. + +**Path Parameters:** +- `id` (string, GUID) - Discount code ID + +**Query Parameters:** +- `page` (integer, optional) - Page number (default: 1) +- `pageSize` (integer, optional) - Items per page (default: 50, max: 100) + +**Response:** +```json +{ + "success": true, + "data": { + "items": [ + { + "id": "usage-id-1", + "discountCodeId": "aa0e8400-e29b-41d4-a716-446655440000", + "tenantId": "tenant-id-1", + "tenantName": "Acme Corp", + "subscriptionId": "sub-id-1", + "tierId": "tier-id-1", + "tierName": "Professional", + "discountApplied": 20.00, + "appliedAt": "2026-02-01T10:00:00Z", + "appliedBy": "admin-user-id" + } + ], + "pagination": { + "currentPage": 1, + "pageSize": 50, + "totalCount": 45, + "totalPages": 1 + } + } +} +``` + +**Status Codes:** +- `200 OK` - Usage history retrieved successfully +- `400 Bad Request` - Invalid discount code ID +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Discount code not found + +--- + +### POST /discount-codes + +Create a new discount code. + +**Request:** +```json +{ + "codeName": "NEWYEAR2026", + "description": "New Year promotional discount", + "discountType": "percentage", + "discountValue": 15, + "applicability": "all", + "applicableTiers": ["tier-id-1", "tier-id-2"], + "isSystemWide": true, + "tenantRestrictions": [], + "expiresAt": "2026-01-31T23:59:59Z", + "usageLimit": 50, + "isActive": true +} +``` + +**Validations:** +- `codeName`: Required, 4-20 characters, alphanumeric, must be unique +- `description`: Required, 1-500 characters +- `discountType`: Required, 'percentage' or 'fixed_amount' +- `discountValue`: Required, > 0; for percentage: <= 100 +- `applicability`: Required, 'new_tenants', 'renewals', or 'all' +- `applicableTiers`: Required array (can be empty for no restrictions) +- `isSystemWide`: Required boolean +- `tenantRestrictions`: Optional array of tenant IDs +- `expiresAt`: Optional ISO 8601 date, must be future date +- `usageLimit`: Optional integer, > 0 +- `isActive`: Required boolean + +**Response:** +```json +{ + "success": true, + "data": { + "id": "bb0e8400-e29b-41d4-a716-446655440000", + "codeName": "NEWYEAR2026", + "description": "New Year promotional discount", + "discountType": "percentage", + "discountValue": 15, + "applicability": "all", + "applicableTiers": ["tier-id-1", "tier-id-2"], + "isSystemWide": true, + "tenantRestrictions": [], + "expiresAt": "2026-01-31T23:59:59Z", + "usageLimit": 50, + "currentUsage": 0, + "isActive": true, + "createdAt": "2026-02-04T12:00:00Z", + "createdBy": "admin-user-id" + } +} +``` + +**Status Codes:** +- `201 Created` - Discount code created successfully +- `400 Bad Request` - Validation errors +- `401 Unauthorized` - Missing or invalid admin token +- `409 Conflict` - Code name already exists + +**Implementation:** +- **Controller:** `DiscountCodeController.CreateDiscountCode` +- **Command:** `CreateDiscountCodeCommand` +- **Handler:** `CreateDiscountCodeCommandHandler` + +--- + +### PATCH /discount-codes/{id} + +Update an existing discount code (partial update supported). + +**Path Parameters:** +- `id` (string, GUID) - Discount code ID + +**Request:** +```json +{ + "description": "Updated description", + "expiresAt": "2026-03-31T23:59:59Z", + "usageLimit": 100, + "isActive": false +} +``` + +**Validations:** +- Same as creation validations (all fields optional for PATCH) +- Cannot change `codeName` after creation +- Cannot change `discountType` after creation + +**Response:** +```json +{ + "success": true, + "data": { + "id": "bb0e8400-e29b-41d4-a716-446655440000", + "codeName": "NEWYEAR2026", + "description": "Updated description", + "discountType": "percentage", + "discountValue": 15, + "applicability": "all", + "applicableTiers": ["tier-id-1", "tier-id-2"], + "isSystemWide": true, + "tenantRestrictions": [], + "expiresAt": "2026-03-31T23:59:59Z", + "usageLimit": 100, + "currentUsage": 15, + "isActive": false, + "createdAt": "2026-02-04T12:00:00Z", + "createdBy": "admin-user-id", + "updatedAt": "2026-02-05T10:00:00Z" + } +} +``` + +**Status Codes:** +- `200 OK` - Discount code updated successfully +- `400 Bad Request` - Validation errors +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Discount code not found + +**Implementation:** +- **Controller:** `DiscountCodeController.UpdateDiscountCode` +- **Command:** `UpdateDiscountCodeCommand` +- **Handler:** `UpdateDiscountCodeCommandHandler` + +--- + +### POST /discount-codes/{id}/enable + +Enable a discount code (make it active). + +**Path Parameters:** +- `id` (string, GUID) - Discount code ID + +**Response:** +```json +{ + "success": true, + "data": { + "id": "bb0e8400-e29b-41d4-a716-446655440000", + "codeName": "NEWYEAR2026", + "isActive": true, + "updatedAt": "2026-02-05T10:00:00Z" + } +} +``` + +**Status Codes:** +- `200 OK` - Discount code enabled successfully +- `400 Bad Request` - Invalid discount code ID or already enabled +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Discount code not found + +--- + +### POST /discount-codes/{id}/disable + +Disable a discount code (make it inactive). + +**Path Parameters:** +- `id` (string, GUID) - Discount code ID + +**Response:** +```json +{ + "success": true, + "data": { + "id": "bb0e8400-e29b-41d4-a716-446655440000", + "codeName": "NEWYEAR2026", + "isActive": false, + "updatedAt": "2026-02-05T10:00:00Z" + } +} +``` + +**Status Codes:** +- `200 OK` - Discount code disabled successfully +- `400 Bad Request` - Invalid discount code ID or already disabled +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Discount code not found + +--- + +### DELETE /discount-codes/{id} + +Delete a discount code (soft delete - preserves usage history). + +**Path Parameters:** +- `id` (string, GUID) - Discount code ID + +**Response:** +```json +{ + "success": true +} +``` + +**Status Codes:** +- `204 No Content` - Discount code deleted successfully +- `400 Bad Request` - Invalid discount code ID or cannot delete (active usage) +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Discount code not found + +**Notes:** +- Soft delete preserves usage history +- Cannot delete codes with active subscriptions using them +- Audit trail maintained for all deletions + +--- + +### POST /discount-codes/validate + +Validate a discount code for a specific subscription context. + +**Request:** +```json +{ + "Code": "WELCOME20", + "TierId": "tier-id-1", + "Frequency": "monthly", + "tenantId": "tenant-id-1" +} +``` + +**Validations:** +- `Code`: Required, 4-20 characters +- `TierId`: Required, valid GUID +- `Frequency`: Required, 'monthly' or 'yearly' +- `tenantId`: Optional, valid GUID + +**Response (Valid):** +```json +{ + "success": true, + "data": { + "valid": true, + "code": { + "id": "aa0e8400-e29b-41d4-a716-446655440000", + "codeName": "WELCOME20", + "discountType": "percentage", + "discountValue": 20, + "description": "20% discount for new customers" + } + } +} +``` + +**Response (Invalid):** +```json +{ + "success": true, + "data": { + "valid": false, + "reason": "Code has expired" + } +} +``` + +**Validation Rules:** +- Code must be active (`isActive: true`) +- Code must not be expired +- If `usageLimit` is set, must not be exceeded +- Tenant must match `applicability` rules +- Tier must be in `applicableTiers` (if restricted) +- Tenant must not be in `tenantRestrictions` + +**Status Codes:** +- `200 OK` - Validation result returned +- `400 Bad Request` - Invalid request parameters +- `401 Unauthorized` - Missing or invalid admin token + +--- + +## System Settings Management + +These endpoints manage system-wide configuration settings for the PurposePath platform. Settings control operational parameters, security policies, email configuration, payment processing, and feature flags. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `Content-Type: application/json` + +--- + +### GET /settings + +Get all system settings as a flat array of individual setting objects. + +**Query Parameters:** + +- `category` (string, optional) - Filter by category ("authentication", "email", "billing", "system") +- `search` (string, optional) - Search settings by key or description + +**Response (200 OK):** + +```json +{ + "success": true, + "data": [ + { + "key": "general.appName", + "value": "PurposePath", + "dataType": "string", + "category": "system", + "description": "The name of the application displayed to users", + "defaultValue": "PurposePath", + "isActive": true, + "lastModifiedAt": "2026-02-05T01:08:46Z", + "lastModifiedBy": "system" + }, + { + "key": "security.passwordMinLength", + "value": "8", + "dataType": "number", + "category": "authentication", + "description": "Minimum required length for user passwords", + "defaultValue": "8", + "isActive": true, + "lastModifiedAt": "2026-02-05T01:08:46Z", + "lastModifiedBy": "admin@example.com" + }, + { + "key": "security.passwordRequireUppercase", + "value": "true", + "dataType": "boolean", + "category": "authentication", + "description": "Require at least one uppercase letter in passwords", + "defaultValue": "true", + "isActive": true, + "lastModifiedAt": "2026-02-05T01:08:46Z", + "lastModifiedBy": "system" + }, + { + "key": "email.provider", + "value": "aws-ses", + "dataType": "string", + "category": "email", + "description": "Email service provider (aws-ses, sendgrid, smtp)", + "defaultValue": "aws-ses", + "isActive": true, + "lastModifiedAt": "2026-02-05T01:08:46Z", + "lastModifiedBy": "system" + }, + { + "key": "payment.trialPeriodDays", + "value": "14", + "dataType": "number", + "category": "billing", + "description": "Number of days for trial subscriptions", + "defaultValue": "14", + "isActive": true, + "lastModifiedAt": "2026-02-05T01:08:46Z", + "lastModifiedBy": "system" + } + ] +} +``` + +**SystemSetting Object Fields:** + +| Field | Type | Description | +|-------|------|-------------| +| `key` | string | Unique setting identifier (e.g., "general.appName") | +| `value` | string | Current value (stored as string, converted based on dataType) | +| `dataType` | string | Data type: "boolean", "string", "number", "json" | +| `category` | string | Setting category: "authentication", "email", "billing", "system" | +| `description` | string | Human-readable description of the setting | +| `defaultValue` | string | Default value if reset | +| `isActive` | boolean | Whether setting is active | +| `lastModifiedAt` | string | ISO 8601 timestamp of last modification | +| `lastModifiedBy` | string | Email of admin who last modified | + +**Setting Categories:** + +| Category | Description | Example Keys | +|----------|-------------|--------------| +| `authentication` | Security and authentication policies | passwordMinLength, maxLoginAttempts, sessionTimeout | +| `email` | Email delivery configuration | provider, fromAddress, fromName, smtpHost | +| `billing` | Payment processing settings | trialPeriodDays, billingCycleDays, currency, provider | +| `system` | General system configuration | appName, companyName, timezone, maintenanceMode | + +**Status Codes:** + +- `200 OK` - Settings retrieved successfully +- `400 Bad Request` - Invalid category value +- `401 Unauthorized` - Missing or invalid admin token +- `403 Forbidden` - User lacks admin role +- `500 Internal Server Error` - Server error + +**Implementation:** + +- Controller: `SystemSettingsController.ListSettings()` +- Query: `ListSystemSettingsQuery` +- Handler: `ListSystemSettingsQueryHandler` + +**Notes:** + +- Response follows standard ApiResponse wrapper format with `success` and `data` fields +- Settings are returned as a flat array (not nested objects) +- All values are stored as strings and converted based on `dataType` +- Sensitive values may be masked in responses +- Frontend uses this endpoint with `useSettings()` hook + +--- + +### GET /settings/{key} + +Get a specific system setting by its key. + +**Path Parameters:** + +- `key` (string, required) - Setting key (e.g., "general.appName", "security.passwordMinLength") + +**Response (200 OK):** + +```json +{ + "success": true, + "data": { + "key": "security.passwordMinLength", + "value": "8", + "dataType": "number", + "category": "authentication", + "description": "Minimum required length for user passwords", + "defaultValue": "8", + "isActive": true, + "lastModifiedAt": "2026-02-05T01:08:46Z", + "lastModifiedBy": "admin@example.com" + } +} +``` + +**Status Codes:** + +- `200 OK` - Setting retrieved successfully +- `400 Bad Request` - Invalid setting key format +- `401 Unauthorized` - Missing or invalid admin token +- `403 Forbidden` - User lacks admin role +- `404 Not Found` - Setting key not found +- `500 Internal Server Error` - Server error + +**Implementation:** + +- Controller: `SystemSettingsController.GetSetting()` +- Query: `GetSystemSettingQuery` +- Handler: `GetSystemSettingQueryHandler` + +--- + +### PATCH /settings/{key} + +Update a specific system setting. + +**Path Parameters:** + +- `key` (string, required) - Setting key (e.g., "general.appName") + +**Request Body:** + +```json +{ + "value": "MyCompany Platform", + "reason": "Rebranding initiative" +} +``` + +**Request Fields:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `value` | string | Yes | New value for the setting (will be validated based on dataType) | +| `reason` | string | Yes | Reason for the change (for audit trail) | + +**Validation Rules by Setting:** + +| Key | Data Type | Constraints | +|-----|-----------|-------------| +| `general.appName` | string | 1-100 characters | +| `general.companyName` | string | 1-200 characters | +| `general.supportEmail` | string | Valid email format | +| `general.timezone` | string | Valid IANA timezone | +| `general.maintenanceMode` | boolean | "true" or "false" | +| `security.passwordMinLength` | number | 6-128 | +| `security.maxLoginAttempts` | number | 1-10 | +| `security.lockoutDuration` | number | 1-1440 (minutes) | +| `email.fromAddress` | string | Valid email format | +| `payment.trialPeriodDays` | number | 0-365 | +| `payment.billingCycleDays` | number | 1, 7, 14, 30, 90, 365 | + +**Response (200 OK):** + +```json +{ + "success": true, + "data": { + "key": "general.appName", + "value": "MyCompany Platform", + "dataType": "string", + "category": "system", + "description": "The name of the application displayed to users", + "defaultValue": "PurposePath", + "isActive": true, + "lastModifiedAt": "2026-02-05T10:30:00Z", + "lastModifiedBy": "admin@example.com" + } +} +``` + +**Status Codes:** + +- `200 OK` - Setting updated successfully +- `400 Bad Request` - Validation error (invalid value for data type, out of range, etc.) +- `401 Unauthorized` - Missing or invalid admin token +- `403 Forbidden` - User lacks admin role +- `404 Not Found` - Setting key not found +- `500 Internal Server Error` - Server error + +**Error Response Example:** + +```json +{ + "success": false, + "error": "Validation failed: Value must be between 6 and 128", + "code": "VALIDATION_ERROR" +} +``` + +**Implementation:** + +- Controller: `SystemSettingsController.UpdateSetting()` +- Command: `UpdateSystemSettingCommand` +- Handler: `UpdateSystemSettingCommandHandler` + +**Notes:** + +- Value is provided as string and validated/converted based on setting's `dataType` +- Changes are logged in audit trail with action type "SETTINGS_UPDATED" +- `reason` field is required for audit compliance +- Some settings may require application restart to take effect + +--- + +### POST /settings/{key}/validate + +Validate a setting value without saving it. + +**Path Parameters:** + +- `key` (string, required) - Setting key to validate + +**Request Body:** + +```json +{ + "value": "5" +} +``` + +**Response (200 OK):** + +```json +{ + "success": true, + "data": { + "valid": true, + "errors": [] + } +} +``` + +**Response (Validation Failed):** + +```json +{ + "success": true, + "data": { + "valid": false, + "errors": [ + "Value must be at least 6", + "Value must be at most 128" + ] + } +} +``` + +**Status Codes:** + +- `200 OK` - Validation completed (check `valid` field in response) +- `400 Bad Request` - Invalid setting key +- `401 Unauthorized` - Missing or invalid admin token +- `403 Forbidden` - User lacks admin role +- `404 Not Found` - Setting key not found +- `500 Internal Server Error` - Server error + +**Implementation:** + +- Controller: `SystemSettingsController.ValidateSetting()` +- Command: `ValidateSystemSettingCommand` +- Handler: `ValidateSystemSettingCommandHandler` + +**Notes:** + +- Performs validation without persisting changes +- Useful for real-time validation in UI forms +- Returns validation errors without saving + +--- + +### POST /settings/{key}/reset + +Reset a specific setting to its default value. + +**Path Parameters:** + +- `key` (string, required) - Setting key to reset + +**Request Body:** + +```json +{ + "reason": "Reverting to default configuration" +} +``` + +**Response (200 OK):** + +```json +{ + "success": true, + "data": { + "key": "security.passwordMinLength", + "value": "8", + "dataType": "number", + "category": "authentication", + "description": "Minimum required length for user passwords", + "defaultValue": "8", + "isActive": true, + "lastModifiedAt": "2026-02-05T11:00:00Z", + "lastModifiedBy": "admin@example.com" + } +} +``` + +**Status Codes:** + +- `200 OK` - Setting reset successfully +- `400 Bad Request` - Invalid setting key or missing reason +- `401 Unauthorized` - Missing or invalid admin token +- `403 Forbidden` - User lacks admin role +- `404 Not Found` - Setting key not found +- `500 Internal Server Error` - Server error + +**Implementation:** + +- Controller: `SystemSettingsController.ResetSetting()` +- Command: `ResetSystemSettingCommand` +- Handler: `ResetSystemSettingCommandHandler` + +**Notes:** + +- Resets the setting to its `defaultValue` +- `reason` is required for audit trail +- Action is logged with "SETTINGS_UPDATED" action type +- Consider impact before resetting critical settings + +--- + +## Role Template Management + +These endpoints manage organizational role templates that tenants can use to quickly set up their organizational structure. Templates contain predefined roles with reporting hierarchies. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `Content-Type: application/json` + +--- + +### GET /role-templates + +Get list of all role templates with optional filtering. + +**Query Parameters:** + +- `search` (string, optional) - Search by template name +- `category` (string, optional) - Filter by category ("STARTUP", "SMB", "ENTERPRISE", "EOS", "SCALING") +- `is_active` (boolean, optional) - Filter by active status + +**Response (200 OK):** + +```json +{ + "success": true, + "data": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "Technology Startup", + "description": "Basic organizational structure for tech startups", + "category": "STARTUP", + "roles_count": 8, + "is_active": true, + "preview": { + "total_roles": 8, + "sample_roles": ["CEO", "CTO", "CFO"] + }, + "created_at": "2026-01-15T10:00:00Z", + "updated_at": "2026-02-01T14:30:00Z" + } + ] +} +``` + +**Status Codes:** + +- `200 OK` - Templates retrieved successfully +- `400 Bad Request` - Invalid query parameters +- `401 Unauthorized` - Missing or invalid admin token +- `403 Forbidden` - User lacks admin role +- `500 Internal Server Error` - Server error + +**Implementation:** + +- Controller: `RoleTemplatesController.ListTemplates()` +- Query: `ListRoleTemplatesQuery` +- Handler: `ListRoleTemplatesQueryHandler` + +--- + +### GET /role-templates/{id} + +Get a specific role template with all its roles. + +**Path Parameters:** + +- `id` (string, GUID) - Template ID + +**Response (200 OK):** + +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "Technology Startup", + "description": "Complete organizational structure for tech startups", + "category": "STARTUP", + "is_active": true, + "roles": [ + { + "id": "role-uuid", + "code": "CEO", + "name": "Chief Executive Officer", + "description": "Leads the company", + "responsibilities": "Set vision, manage executives...", + "reports_to_code": null, + "created_at": "2026-01-15T10:00:00Z" + }, + { + "id": "role-uuid-2", + "code": "CTO", + "name": "Chief Technology Officer", + "description": "Oversees technology", + "responsibilities": "Manage tech stack, lead dev team...", + "reports_to_code": "CEO", + "created_at": "2026-01-15T10:05:00Z" + } + ], + "created_at": "2026-01-15T10:00:00Z", + "updated_at": "2026-02-01T14:30:00Z" + } +} +``` + +**Status Codes:** + +- `200 OK` - Template retrieved successfully +- `400 Bad Request` - Invalid template ID format +- `401 Unauthorized` - Missing or invalid admin token +- `403 Forbidden` - User lacks admin role +- `404 Not Found` - Template not found +- `500 Internal Server Error` - Server error + +**Implementation:** + +- Controller: `RoleTemplatesController.GetTemplate()` +- Query: `GetRoleTemplateQuery` +- Handler: `GetRoleTemplateQueryHandler` + +--- + +### POST /role-templates + +Create a new role template. + +**Request Body:** + +```json +{ + "name": "Technology Startup", + "description": "Basic tech startup org structure", + "category": "STARTUP", + "is_active": true +} +``` + +**Validation Rules:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| `name` | string | Yes | 1-100 characters, unique | +| `description` | string | No | Max 500 characters | +| `category` | string | Yes | Enum: "STARTUP", "SMB", "ENTERPRISE", "EOS", "SCALING" | +| `is_active` | boolean | No | Default: true | + +**Response (201 Created):** + +```json +{ + "success": true, + "data": { + "id": "new-uuid", + "name": "Technology Startup", + "description": "Basic tech startup org structure", + "category": "STARTUP", + "is_active": true, + "roles": [], + "created_at": "2026-02-05T15:00:00Z", + "updated_at": "2026-02-05T15:00:00Z" + } +} +``` + +**Status Codes:** + +- `201 Created` - Template created successfully +- `400 Bad Request` - Validation error +- `401 Unauthorized` - Missing or invalid admin token +- `403 Forbidden` - User lacks admin role +- `409 Conflict` - Template name already exists +- `500 Internal Server Error` - Server error + +**Implementation:** + +- Controller: `RoleTemplatesController.CreateTemplate()` +- Command: `CreateRoleTemplateCommand` +- Handler: `CreateRoleTemplateCommandHandler` + +--- + +### PUT /role-templates/{id} + +Update an existing role template. + +**Path Parameters:** + +- `id` (string, GUID) - Template ID + +**Request Body:** + +```json +{ + "name": "Updated Template Name", + "description": "Updated description", + "category": "ENTERPRISE", + "is_active": false +} +``` + +**Notes:** + +- All fields are optional (partial update supported) +- Cannot change template ID + +**Response (200 OK):** + +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "Updated Template Name", + "description": "Updated description", + "category": "ENTERPRISE", + "is_active": false, + "roles": [...], + "created_at": "2026-01-15T10:00:00Z", + "updated_at": "2026-02-05T15:30:00Z" + } +} +``` + +**Status Codes:** + +- `200 OK` - Template updated successfully +- `400 Bad Request` - Validation error +- `401 Unauthorized` - Missing or invalid admin token +- `403 Forbidden` - User lacks admin role +- `404 Not Found` - Template not found +- `409 Conflict` - Duplicate name +- `500 Internal Server Error` - Server error + +**Implementation:** + +- Controller: `RoleTemplatesController.UpdateTemplate()` +- Command: `UpdateRoleTemplateCommand` +- Handler: `UpdateRoleTemplateCommandHandler` + +--- + +### DELETE /role-templates/{id} + +Delete a role template. + +**Path Parameters:** + +- `id` (string, GUID) - Template ID + +**Response:** + +``` +204 No Content +``` + +**Status Codes:** + +- `204 No Content` - Template deleted successfully +- `400 Bad Request` - Invalid template ID +- `401 Unauthorized` - Missing or invalid admin token +- `403 Forbidden` - User lacks admin role +- `404 Not Found` - Template not found +- `500 Internal Server Error` - Server error + +**Implementation:** + +- Controller: `RoleTemplatesController.DeleteTemplate()` +- Command: `DeleteRoleTemplateCommand` +- Handler: `DeleteRoleTemplateCommandHandler` + +--- + +### POST /role-templates/{id}/roles + +Add a role to a template. + +**Path Parameters:** + +- `id` (string, GUID) - Template ID + +**Request Body:** + +```json +{ + "code": "VP_SALES", + "name": "Vice President of Sales", + "description": "Leads sales team", + "responsibilities": "* Drive revenue\n* Manage team\n* Set targets", + "reports_to_code": "CEO" +} +``` + +**Validation Rules:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| `code` | string | Yes | 2-50 chars, uppercase, alphanumeric + underscore, unique within template | +| `name` | string | Yes | 1-100 characters | +| `description` | string | No | Max 500 characters | +| `responsibilities` | string | No | Max 2000 characters, markdown supported | +| `reports_to_code` | string | No | Must be a valid role code within same template | + +**Response (201 Created):** + +```json +{ + "success": true, + "data": { + "id": "role-uuid", + "code": "VP_SALES", + "name": "Vice President of Sales", + "description": "Leads sales team", + "responsibilities": "* Drive revenue\n* Manage team\n* Set targets", + "reports_to_code": "CEO", + "created_at": "2026-02-05T15:30:00Z" + } +} +``` + +**Status Codes:** + +- `201 Created` - Role added successfully +- `400 Bad Request` - Validation error (invalid code format, circular reference) +- `401 Unauthorized` - Missing or invalid admin token +- `403 Forbidden` - User lacks admin role +- `404 Not Found` - Template not found +- `409 Conflict` - Role code already exists in template +- `500 Internal Server Error` - Server error + +**Implementation:** + +- Controller: `RoleTemplatesController.AddRole()` +- Command: `AddTemplateRoleCommand` +- Handler: `AddTemplateRoleCommandHandler` + +**Notes:** + +- Role codes must be uppercase (e.g., "CEO", "VP_SALES") +- `reports_to_code` creates hierarchical structure +- System validates against circular references + +--- + +### PUT /role-templates/{id}/roles/{roleId} + +Update a role within a template. + +**Path Parameters:** + +- `id` (string, GUID) - Template ID +- `roleId` (string, GUID) - Role ID + +**Request Body:** + +```json +{ + "name": "Vice President of Sales & Marketing", + "description": "Oversees sales and marketing", + "responsibilities": "Updated responsibilities...", + "reports_to_code": "COO" +} +``` + +**Notes:** + +- All fields are optional (partial update supported) +- Cannot change `code` after creation +- Can set `reports_to_code` to null to remove reporting relationship + +**Response (200 OK):** + +```json +{ + "success": true, + "data": { + "id": "role-uuid", + "code": "VP_SALES", + "name": "Vice President of Sales & Marketing", + "description": "Oversees sales and marketing", + "responsibilities": "Updated responsibilities...", + "reports_to_code": "COO", + "created_at": "2026-01-15T10:05:00Z" + } +} +``` + +**Status Codes:** + +- `200 OK` - Role updated successfully +- `400 Bad Request` - Validation error (circular reference, invalid reports_to_code) +- `401 Unauthorized` - Missing or invalid admin token +- `403 Forbidden` - User lacks admin role +- `404 Not Found` - Template or role not found +- `500 Internal Server Error` - Server error + +**Implementation:** + +- Controller: `RoleTemplatesController.UpdateRole()` +- Command: `UpdateTemplateRoleCommand` +- Handler: `UpdateTemplateRoleCommandHandler` + +--- + +### DELETE /role-templates/{id}/roles/{roleId} + +Remove a role from a template. + +**Path Parameters:** + +- `id` (string, GUID) - Template ID +- `roleId` (string, GUID) - Role ID + +**Response:** + +``` +204 No Content +``` + +**Status Codes:** + +- `204 No Content` - Role deleted successfully +- `400 Bad Request` - Cannot delete role (other roles report to it) +- `401 Unauthorized` - Missing or invalid admin token +- `403 Forbidden` - User lacks admin role +- `404 Not Found` - Template or role not found +- `500 Internal Server Error` - Server error + +**Error Response (Role Has Subordinates):** + +```json +{ + "success": false, + "error": "Cannot delete role 'CEO' because roles ['CTO', 'CFO'] report to it", + "code": "ROLE_HAS_SUBORDINATES" +} +``` + +**Implementation:** + +- Controller: `RoleTemplatesController.DeleteRole()` +- Command: `DeleteTemplateRoleCommand` +- Handler: `DeleteTemplateRoleCommandHandler` + +**Notes:** + +- Cannot delete a role if other roles have `reports_to_code` pointing to it +- Must reassign or delete subordinate roles first + +--- + +## User Management + +Manage user accounts across tenants, including account status operations (unlock, suspend, reactivate). + +### GET /users + +Get paginated list of users with optional filtering. + +**Query Parameters:** +- `page` (integer, optional) - Page number (default: 1) +- `pageSize` (integer, optional) - Items per page (default: 50, max: 100) +- `search` (string, optional) - Search by name or email +- `tenantId` (string, optional) - Filter by tenant ID +- `status` ('Active' | 'Suspended' | 'Locked' | 'Inactive', optional) - Filter by user status + +**Response:** +```json +{ + "success": true, + "data": { + "items": [ + { + "id": "user-id-1", + "email": "user@example.com", + "firstName": "John", + "lastName": "Doe", + "tenantId": "tenant-id-1", + "tenantName": "Acme Corp", + "status": "Active", + "failedLoginAttempts": 0, + "lastLoginAt": "2026-02-04T10:00:00Z", + "createdAt": "2026-01-01T00:00:00Z", + "role": "User" + } + ], + "pagination": { + "currentPage": 1, + "pageSize": 50, + "totalCount": 245, + "totalPages": 5 + } + } +} +``` + +**Status Codes:** +- `200 OK` - Users retrieved successfully +- `400 Bad Request` - Invalid query parameters +- `401 Unauthorized` - Missing or invalid admin token + +**Implementation:** +- **Controller:** `UserController.GetUsers` +- **Query:** `GetUsersQuery` +- **Handler:** `GetUsersQueryHandler` + +--- + +### GET /users/{id} + +Get detailed information for a specific user, including activity history. + +**Path Parameters:** +- `id` (string, GUID) - User ID + +**Response:** +```json +{ + "success": true, + "data": { + "id": "user-id-1", + "email": "user@example.com", + "firstName": "John", + "lastName": "Doe", + "tenantId": "tenant-id-1", + "tenantName": "Acme Corp", + "status": "Active", + "failedLoginAttempts": 0, + "lastLoginAt": "2026-02-04T10:00:00Z", + "createdAt": "2026-01-01T00:00:00Z", + "role": "User", + "activityHistory": { + "loginHistory": [ + { + "timestamp": "2026-02-04T10:00:00Z", + "ipAddress": "192.168.1.1", + "userAgent": "Mozilla/5.0...", + "success": true + } + ], + "featureUsage": [ + { + "feature": "Dashboard", + "usageCount": 150, + "lastUsedAt": "2026-02-04T10:00:00Z" + } + ], + "subscriptionChanges": [ + { + "timestamp": "2026-01-15T10:00:00Z", + "changeType": "upgrade", + "details": "Upgraded to Professional tier", + "performedBy": "admin-user-id" + } + ] + } + } +} +``` + +**Status Codes:** +- `200 OK` - User details retrieved successfully +- `400 Bad Request` - Invalid user ID +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - User not found + +**Implementation:** +- **Controller:** `UserController.GetUserDetails` +- **Query:** `GetUserDetailsQuery` +- **Handler:** `GetUserDetailsQueryHandler` + +--- + +### POST /users/{id}/unlock + +Unlock a user account (reset failed login attempts). + +**Path Parameters:** +- `id` (string, GUID) - User ID + +**Request:** +```json +{ + "reason": "User contacted support, verified identity", + "notifyUser": true +} +``` + +**Validations:** +- `reason`: Required, 1-500 characters +- `notifyUser`: Optional boolean (default: false) + +**Response:** +```json +{ + "success": true +} +``` + +**Status Codes:** +- `200 OK` - Account unlocked successfully +- `400 Bad Request` - Invalid user ID or account not locked +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - User not found + +**Implementation:** +- **Controller:** `UserController.UnlockAccount` +- **Command:** `UnlockAccountCommand` +- **Handler:** `UnlockAccountCommandHandler` + +**Notes:** +- Resets `failedLoginAttempts` to 0 +- Changes status from 'Locked' to 'Active' +- Optionally sends email notification to user +- Audit log entry created with admin ID and reason + +--- + +### POST /users/{id}/suspend + +Suspend a user account (prevent login). + +**Path Parameters:** +- `id` (string, GUID) - User ID + +**Request:** +```json +{ + "reason": "Suspicious activity detected", + "notifyUser": true +} +``` + +**Validations:** +- `reason`: Required, 1-500 characters +- `notifyUser`: Optional boolean (default: false) + +**Response:** +```json +{ + "success": true +} +``` + +**Status Codes:** +- `200 OK` - Account suspended successfully +- `400 Bad Request` - Invalid user ID or account already suspended +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - User not found + +**Implementation:** +- **Controller:** `UserController.SuspendAccount` +- **Command:** `SuspendAccountCommand` +- **Handler:** `SuspendAccountCommandHandler` + +**Notes:** +- Changes status to 'Suspended' +- User cannot log in while suspended +- Optionally sends email notification to user +- Audit log entry created with admin ID and reason + +--- + +### POST /users/{id}/reactivate + +Reactivate a suspended user account. + +**Path Parameters:** +- `id` (string, GUID) - User ID + +**Request:** +```json +{ + "reason": "Issue resolved, account verified", + "notifyUser": true +} +``` + +**Validations:** +- `reason`: Required, 1-500 characters +- `notifyUser`: Optional boolean (default: false) + +**Response:** +```json +{ + "success": true +} +``` + +**Status Codes:** +- `200 OK` - Account reactivated successfully +- `400 Bad Request` - Invalid user ID or account not suspended +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - User not found + +**Implementation:** +- **Controller:** `UserController.ReactivateAccount` +- **Command:** `ReactivateAccountCommand` +- **Handler:** `ReactivateAccountCommandHandler` + +**Notes:** +- Changes status from 'Suspended' to 'Active' +- User can log in normally after reactivation +- Optionally sends email notification to user +- Audit log entry created with admin ID and reason + +--- + +## Audit Log Management + +Comprehensive audit logging for all administrative actions. All write operations are automatically logged with admin user ID, action details, timestamps, and contextual metadata. + +### GET /audit-logs + +Get paginated list of audit log entries with optional filtering. + +**Query Parameters:** +- `page` (integer, optional) - Page number (default: 1) +- `pageSize` (integer, optional) - Items per page (default: 50, max: 100) +- `adminEmail` (string, optional) - Filter by admin user email +- `actionType` (string, optional) - Filter by action type (e.g., 'CREATE', 'UPDATE', 'DELETE') +- `tenantId` (string, optional) - Filter by affected tenant ID +- `startDate` (string, optional) - Filter by date range start (ISO 8601) +- `endDate` (string, optional) - Filter by date range end (ISO 8601) + +**Response:** +```json +{ + "items": [ + { + "id": "log-id-1", + "adminUserId": "admin-user-id", + "adminEmail": "admin@purposepath.app", + "actionType": "UPDATE", + "resourceType": "SystemSetting", + "resourceId": "MaxLoginAttempts", + "message": "Updated system setting MaxLoginAttempts from 5 to 3", + "metadata": { + "oldValue": "5", + "newValue": "3", + "reason": "Security policy update" + }, + "tenantId": null, + "ipAddress": "192.168.1.1", + "timestamp": "2026-02-04T10:00:00Z" + } + ], + "pagination": { + "currentPage": 1, + "pageSize": 50, + "totalCount": 1234, + "totalPages": 25 + } +} +``` + +**Status Codes:** +- `200 OK` - Audit logs retrieved successfully +- `400 Bad Request` - Invalid query parameters +- `401 Unauthorized` - Missing or invalid admin token + +**Implementation:** +- **Controller:** `AuditLogController.GetAuditLogs` +- **Query:** `GetAuditLogsQuery` +- **Handler:** `GetAuditLogsQueryHandler` + +**Notes:** +- All write operations automatically create audit entries +- Includes detailed metadata for each action +- Filterable by admin, action type, tenant, and date range +- Used for compliance, security auditing, and troubleshooting + +--- + +### GET /audit-logs/{id} + +Get a single audit log entry by ID. + +**Path Parameters:** +- `id` (string, GUID) - Audit log entry ID + +**Response:** +```json +{ + "id": "log-id-1", + "adminUserId": "admin-user-id", + "adminEmail": "admin@purposepath.app", + "actionType": "UPDATE", + "resourceType": "SystemSetting", + "resourceId": "MaxLoginAttempts", + "message": "Updated system setting MaxLoginAttempts from 5 to 3", + "metadata": { + "oldValue": "5", + "newValue": "3", + "reason": "Security policy update" + }, + "tenantId": null, + "ipAddress": "192.168.1.1", + "timestamp": "2026-02-04T10:00:00Z" +} +``` + +**Status Codes:** +- `200 OK` - Audit log entry retrieved successfully +- `400 Bad Request` - Invalid audit log ID +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Audit log entry not found + +--- + +### GET /audit-logs/export + +Export audit logs to CSV format. + +**Query Parameters:** +- Same filters as GET /audit-logs (without pagination) +- `adminEmail` (string, optional) - Filter by admin user email +- `actionType` (string, optional) - Filter by action type +- `tenantId` (string, optional) - Filter by affected tenant ID +- `startDate` (string, optional) - Filter by date range start (ISO 8601) +- `endDate` (string, optional) - Filter by date range end (ISO 8601) + +**Response:** +- Content-Type: `text/csv` +- CSV file with headers: ID, Admin Email, Action Type, Resource Type, Resource ID, Message, Timestamp, IP Address + +**Status Codes:** +- `200 OK` - CSV export generated successfully +- `400 Bad Request` - Invalid query parameters +- `401 Unauthorized` - Missing or invalid admin token + +**Implementation:** +- **Controller:** `AuditLogController.ExportAuditLogs` +- **Query:** `ExportAuditLogsQuery` +- **Handler:** `ExportAuditLogsQueryHandler` + +**Notes:** +- Large exports may take time to generate +- Filtered by same criteria as list endpoint +- Useful for compliance reporting and analysis + +--- + +### GET /audit-logs/action-types + +Get list of available action types for filtering. + +**Response:** +```json +[ + "CREATE", + "UPDATE", + "DELETE", + "EXTEND_TRIAL", + "APPLY_DISCOUNT", + "GRANT_FEATURE", + "UNLOCK_ACCOUNT", + "SUSPEND_ACCOUNT", + "REACTIVATE_ACCOUNT", + "RESET_SETTING", + "VALIDATE_SETTING" +] +``` + +**Status Codes:** +- `200 OK` - Action types retrieved successfully +- `401 Unauthorized` - Missing or invalid admin token + +**Notes:** +- Dynamically generated from actual audit log entries +- Used to populate filter dropdowns in UI +- Action types are automatically added as new operations are logged + +--- + +## People Management + +Manage people within specific tenants (admin cross-tenant access). + +### GET /tenants/{tenantId}/people + +Get paginated list of people within a specific tenant. + +**Path Parameters:** +- `tenantId` (string, GUID) - Tenant ID + +**Query Parameters:** +- `pageNumber` (integer, optional): Page number (default: 1) +- `pageSize` (integer, optional): Items per page (default: 20, max: 100) +- `personTypeId` (string, GUID, optional): Filter by person type ID +- `status` (string, optional): Filter by status ('active' or 'inactive') +- `tagId` (string, GUID, optional): Filter by tag ID +- `search` (string, optional): Search term (firstName, lastName, email) +- `includeRoles` (boolean, optional): Include role assignments (default: false) + +**Response:** +```json +{ + "items": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@acme.com", + "isEmailVerified": true, + "phone": "+1234567890", + "title": "Chief Executive Officer", + "personType": { + "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", + "name": "Employee", + "description": "Full-time employee" + }, + "isActive": true, + "isAssignable": true, + "primaryRole": { + "id": "8d9e6679-7425-40de-944b-e07fc1f90ae8", + "name": "CEO", + "description": "Chief Executive Officer" + }, + "tags": [ + { + "id": "9e0f6679-7425-40de-944b-e07fc1f90ae9", + "name": "Leadership", + "color": "#FF5722" + } + ], + "hasSystemAccess": true, + "createdAt": "2025-01-15T10:30:00Z", + "updatedAt": "2025-01-20T14:45:00Z" + } + ], + "pagination": { + "currentPage": 1, + "pageSize": 20, + "totalCount": 45, + "totalPages": 3 + } +} +``` + +**Status Codes:** +- `200 OK` - People retrieved successfully +- `400 Bad Request` - Invalid query parameters +- `401 Unauthorized` - Missing or invalid admin token +- `404 Not Found` - Tenant not found + +**Notes:** +- Admin can view people from any tenant (cross-tenant access) +- Same filtering options as user-facing endpoint +- Results include system access status (user account linkage) +- Role information included when `includeRoles=true` +- Audit log entry created + +--- + +## Data Models + +### SeedingResult + +```typescript +interface SeedingResult { + itemsCreated: number; + itemsSkipped: number; + errors: string[]; +} +``` + +### PaginationInfo + +```typescript +interface PaginationInfo { + currentPage: number; + pageSize: number; + totalCount: number; + totalPages: number; +} +``` + +### SubscriptionTier + +```typescript +interface SubscriptionTier { + id: string; // GUID + name: string; + displayName: string; + description: string; + pricing: { + monthlyPrice: number; + yearlyPrice: number; + currency: string; // ISO currency code + }; + features: FeatureName[]; + limits: { + goals: number | null; // null = unlimited + actions: number | null; + strategies: number | null; + measures: number | null; + attachments: number | null; + reports: number | null; + }; + supportedFrequencies: SubscriptionFrequency[]; + isActive: boolean; + sortOrder: number; + createdAt: string; // ISO 8601 + updatedAt: string; // ISO 8601 +} +``` + +### FeatureName (Enum) + +```typescript +enum FeatureName { + Goals = "Goals", + Operations = "Operations", + Measures = "Measures", + Strategies = "Strategies", + Realtime = "Realtime", + Reports = "Reports", + Attachments = "Attachments", + BulkPlanner = "BulkPlanner", + StrategyCompare = "StrategyCompare", + GoalCreate = "GoalCreate" +} +``` + +### SubscriptionStatus (Enum) + +```typescript +enum SubscriptionStatus { + Active = "Active", + Trial = "Trial", + Cancelled = "Cancelled", + Suspended = "Suspended", + PastDue = "PastDue" +} +``` + +### SubscriptionFrequency (Enum) + +```typescript +enum SubscriptionFrequency { + Monthly = "Monthly", + Yearly = "Yearly" +} +``` + +### StatusCategory (Enum) + +```typescript +enum StatusCategory { + Open = "Open", + InProgress = "InProgress", + Resolved = "Resolved", + Closed = "Closed" +} +``` + +### VariableType (Enum) + +```typescript +enum VariableType { + String = "String", + Number = "Number", + Boolean = "Boolean", + Date = "Date", + Url = "Url", + Email = "Email" +} +``` + +--- + +## Error Handling + +### Standard Error Response + +```json +{ + "success": false, + "error": "Human-readable error message" +} +``` + +### Common HTTP Status Codes + +| Code | Meaning | When Used | +|------|---------|-----------| +| 200 | OK | Successful GET, PUT, POST (non-creation) | +| 201 | Created | Successful POST (resource creation) | +| 204 | No Content | Successful DELETE | +| 400 | Bad Request | Invalid request data, validation errors | +| 401 | Unauthorized | Missing or invalid authentication token | +| 403 | Forbidden | Valid token but insufficient permissions | +| 404 | Not Found | Resource not found | +| 409 | Conflict | Duplicate resource, constraint violation | +| 500 | Internal Server Error | Unexpected server error | + +### Validation Errors + +Validation errors return 400 Bad Request with detailed field-level errors: + +```json +{ + "success": false, + "error": "Validation failed", + "details": [ + { + "field": "email", + "message": "Invalid email format" + }, + { + "field": "monthlyPrice", + "message": "Must be a positive number" + } + ] +} +``` + +### Business Rule Violations + +Business rule violations return 400 Bad Request with context: + +```json +{ + "success": false, + "error": "Cannot delete plan with active subscriptions", + "details": { + "planId": "550e8400-e29b-41d4-a716-446655440001", + "activeSubscribers": 142, + "action": "Use POST /plans/{id}/deactivate instead" + } +} +``` + +--- + +## Summary + +**Total Endpoints:** 119 + +**Breakdown by Category:** +- Health & System: 1 +- Authentication: 2 +- System Seeding: 7 +- Issue Type Configuration: 7 +- Issue Status Configuration: 6 +- Email Template Management: 9 +- Subscriber Management: 2 +- Plan Management: 8 +- Feature Management: 12 +- Subscription Operations: 6 +- Discount Code Management: 9 +- System Settings Management: 5 +- Role Template Management: 8 +- User Management: 5 +- Audit Log Management: 4 +- People Management: 1 + +**All endpoints require Admin role authorization except:** +- GET /health +- POST /auth/validate +- POST /auth/refresh + +**Audit Logging:** All write operations automatically logged with admin user ID, action details, and timestamps. + +**Pattern Consistency:** All endpoints follow standard patterns for pagination, filtering, error handling, and response envelopes. + +--- + +## Related Documentation + +- **[Backend Development Guidelines](../../.github/DEVELOPMENT_GUIDELINES.md)** - Architecture & coding standards +- **[Copilot Rules](../../.github/COPILOT_RULES.md)** - Specification enforcement rules +- **[User App Specifications](../user-app/index.md)** - User-facing API specifications + +--- + +**End of Specification** diff --git a/docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md b/docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md new file mode 100644 index 00000000..f6aa73cf --- /dev/null +++ b/docs/shared/Specifications/ai-admin-portal/admin_ai_specifications.md @@ -0,0 +1,1127 @@ +# Admin AI Specifications - LLM Topic Management + +- Last Updated: January 30, 2026 +- Version: 3.0 + +## Revision History + +| Date | Version | Description | +|------|---------|-------------| +| 2026-01-30 | 3.0 | **Issue #158 Completion:** Added tier-based LLM model selection and topic access control. Replaced `model_code` with `basic_model_code` and `premium_model_code`. Added `tier_level` field (FREE, BASIC, PREMIUM, ULTIMATE). | +| 2026-01-25 | 2.0 | **Issue #196 Completion:** Fixed category enum values to match actual TopicCategory implementation, verified all field values match constants.py | +| 2025-12-25 | 1.0 | Initial admin specification | + +--- + +## Overview + +This document specifies all admin endpoints for managing the LLM Topic system. Admin users can update topic configurations, manage prompts, and test topics. + +**Important:** Topics are defined in the code-based `endpoint_registry` and cannot be created or deleted by admins. Admins can only: +- Update topic configurations (tier level, dual LLM models, temperature, prompts, etc.) +- Manage prompt content (system, user, assistant prompts) +- Test topic configurations before activation + +### Tier-Based Access Control (Issue #158) + +Each topic has a `tier_level` that controls: +1. **Topic Access**: Which subscription tiers can access the topic +2. **Model Selection**: Which LLM model to use based on user's tier + +**Tier Levels:** +- **FREE**: Users can access only FREE topics, uses `basic_model_code` +- **BASIC**: Users can access FREE + BASIC topics, uses `basic_model_code` +- **PREMIUM**: Users can access FREE + BASIC + PREMIUM topics, uses `premium_model_code` +- **ULTIMATE**: Users can access all topics, uses `premium_model_code` + +**Dual Model Configuration:** +- `basic_model_code`: LLM model for FREE and BASIC tier users +- `premium_model_code`: LLM model for PREMIUM and ULTIMATE tier users +- Admins can set different models for each tier (e.g., Claude Haiku for basic, Claude Sonnet for premium) + +--- + +## Implementation Status + +| Endpoint | Status | Notes | +|----------|--------|-------| +| GET /topics | ✅ Implemented | List topics from registry + DB overrides | +| GET /topics/{topic_id} | ✅ Implemented | | +| PUT /topics/{topic_id} | ✅ Implemented | Update topic config | +| GET /topics/{topic_id}/prompts/{prompt_type} | ✅ Implemented | | +| PUT /topics/{topic_id}/prompts/{prompt_type} | ✅ Implemented | | +| POST /topics/{topic_id}/prompts | ✅ Implemented | | +| DELETE /topics/{topic_id}/prompts/{prompt_type} | ✅ Implemented | | +| GET /models | ✅ Implemented | | +| POST /topics/validate | ✅ Implemented | | +| POST /topics/{topic_id}/test | ✅ Implemented | **New** - Test with auto-enrichment | +| GET /topics/{topic_id}/stats | ⏳ Planned | Usage statistics | + +--- + +## Authentication + +All admin endpoints require: + +- **Authentication**: Bearer token with admin role +- **Authorization**: `admin:topics:*` permission scope +- **Headers**: + - `Authorization: Bearer {token}` + - `Content-Type: application/json` + +--- + +## Endpoints + +### 1. List Topics + +**Purpose:** Get all topics (active and inactive) for admin management + +**Endpoint:** + +```http +GET /api/v1/admin/topics +``` + +**Query Parameters:** + +| Parameter | Type | Required | Default | Description | Allowed Values | +|-----------|------|----------|---------|-------------|----------------| +| `page` | integer | No | 1 | Page number | >= 1 | +| `page_size` | integer | No | 50 | Items per page (max 100) | 1-100 | +| `category` | string | No | - | Filter by category | `onboarding`, `conversation`, `insights`, `strategic_planning`, `operations_ai`, `operations_strategic_integration`, `analysis` | +| `topic_type` | string | No | - | Filter by type | `conversation_coaching`, `single_shot`, `measure_system` | +| `is_active` | boolean | No | - | Filter by active status | `true`, `false` | +| `search` | string | No | - | Search in name/description | Max 100 chars | + +**Response:** + +```json +{ + "topics": [ + { + "topic_id": "core_values_coaching", + "topic_name": "Core Values - Coaching Session", + "category": "core_values", + "topic_type": "conversation_coaching", + "tier_level": "free", + "basic_model_code": "claude-3-5-sonnet-20241022", + "premium_model_code": "claude-3-5-sonnet-20241022", + "temperature": 0.7, + "max_tokens": 2000, + "is_active": true, + "description": "Explore core values through conversation", + "display_order": 1, + "from_database": true, + "templates": [ + {"prompt_type": "system", "is_defined": true}, + {"prompt_type": "user", "is_defined": false} + ], + "created_at": "2024-11-01T10:00:00Z", + "updated_at": "2024-11-13T15:30:00Z", + "created_by": "admin_123" + } + ], + "total": 12, + "page": 1, + "page_size": 50, + "has_more": false +} +``` + +**Response Field Descriptions:** + +| Field | Description | +|-------|-------------| +| `from_database` | `true` = Topic config stored in DB, `false` = Using registry defaults | +| `templates` | Array of allowed templates with `is_defined` indicating if uploaded to S3 | + +**Status Codes:** + +- `200 OK`: Success +- `401 Unauthorized`: Missing or invalid auth token +- `403 Forbidden`: Insufficient permissions + +--- + +### 2. Get Topic Details + +**Purpose:** Get complete details for a specific topic including prompts and allowed parameters + +**Endpoint:** + +```http +GET /api/v1/admin/topics/{topic_id} +``` + +**Path Parameters:** + +| Parameter | Type | Required | Description | Format | +|-----------|------|----------|-------------|--------| +| `topic_id` | string | Yes | Unique topic identifier | snake_case, 3-50 chars | + +**Query Parameters:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `include_schema` | boolean | No | `false` | Include JSON schema of the response model for template design | + +**Response:** + +```json +{ + "topic_id": "core_values_coaching", + "topic_name": "Core Values - Coaching Session", + "category": "core_values", + "topic_type": "conversation_coaching", + "description": "Explore your core values through conversation", + "tier_level": "free", + "basic_model_code": "claude-3-5-sonnet-20241022", + "premium_model_code": "claude-3-5-sonnet-20241022", + "temperature": 0.7, + "max_tokens": 2000, + "top_p": 1.0, + "frequency_penalty": 0.0, + "presence_penalty": 0.0, + "is_active": true, + "display_order": 1, + "from_database": true, + "prompts": [ + { + "prompt_type": "system", + "s3_bucket": "purposepath-prompts-prod", + "s3_key": "prompts/core_values_coaching/system.md", + "updated_at": "2024-11-13T15:30:00Z", + "updated_by": "admin_123" + }, + { + "prompt_type": "user", + "s3_bucket": "purposepath-prompts-prod", + "s3_key": "prompts/core_values_coaching/user.md", + "updated_at": "2024-11-13T15:30:00Z", + "updated_by": "admin_123" + } + ], + "template_status": [ + { + "prompt_type": "system", + "is_defined": true, + "s3_bucket": "purposepath-prompts-prod", + "s3_key": "prompts/core_values_coaching/system.md", + "updated_at": "2024-11-13T15:30:00Z", + "updated_by": "admin_123" + }, + { + "prompt_type": "user", + "is_defined": false, + "s3_bucket": null, + "s3_key": null, + "updated_at": null, + "updated_by": null + } + ], + "allowed_parameters": [ + { + "name": "user_name", + "type": "string", + "required": true, + "description": "User's display name" + }, + { + "name": "core_values", + "type": "string", + "required": false, + "description": "User's defined core values (auto-enriched from profile)" + }, + { + "name": "purpose", + "type": "string", + "required": false, + "description": "User's purpose statement (auto-enriched from profile)" + } + ], + "conversation_config": { + "max_messages_to_llm": 30, + "inactivity_timeout_minutes": 30, + "session_ttl_days": 14, + "estimated_messages": 20, + "extraction_model_code": "CLAUDE_3_5_HAIKU" + }, + "response_schema": null, + "created_at": "2024-11-01T10:00:00Z", + "updated_at": "2024-11-13T15:30:00Z", + "created_by": "admin_123" +} +``` + +**Response Schema (when `include_schema=true`):** + +When the `include_schema` query parameter is set to `true`, the response includes the JSON schema of the expected response model. This is useful for template authors to understand what output fields their prompts should generate. + +Example with `include_schema=true`: + +```json +{ + "topic_id": "niche_review", + "...": "...other fields...", + "response_schema": { + "title": "OnboardingReviewResponse", + "type": "object", + "properties": { + "strengths": { + "type": "array", + "items": {"type": "string"}, + "description": "List of strengths identified" + }, + "weaknesses": { + "type": "array", + "items": {"type": "string"}, + "description": "List of weaknesses or areas for improvement" + }, + "recommendations": { + "type": "array", + "items": {"type": "string"}, + "description": "List of actionable recommendations" + }, + "alignment_score": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "description": "Overall alignment score" + }, + "summary": { + "type": "string", + "description": "Summary of the analysis" + } + }, + "required": ["strengths", "weaknesses", "recommendations", "alignment_score", "summary"] + } +} +``` + +**Note:** The `response_schema` is `null` when: +- `include_schema=false` (default) +- The topic is not in the endpoint registry (custom topics) +- The response model is not registered in the response model registry + +**Conversation Config (conversation_coaching topics only):** + +For topics with `topic_type: "conversation_coaching"`, the response includes `conversation_config`: + +| Field | Type | Range | Default | Description | +|-------|------|-------|---------|-------------| +| `max_messages_to_llm` | integer | 5-100 | 30 | Maximum messages to include in LLM context (sliding window) | +| `inactivity_timeout_minutes` | integer | 5-1440 | 30 | Minutes of inactivity before session auto-pauses | +| `session_ttl_days` | integer | 1-90 | 14 | Days to keep paused/completed sessions before deletion | +| `estimated_messages` | integer | 5-100 | 20 | Estimated messages for a typical session (for progress calculation) | +| `extraction_model_code` | string | - | CLAUDE_3_5_HAIKU | MODEL_REGISTRY code for extraction (e.g., CLAUDE_3_5_HAIKU, CLAUDE_3_5_SONNET_V2) | + +**Template Status:** + +The `template_status` array shows each allowed template and its definition status: + +| Field | Description | +|-------|-------------| +| `is_defined` | `true` if this prompt has been uploaded to S3 | +| `s3_bucket`, `s3_key` | S3 location (null if not defined) | +| `updated_at`, `updated_by` | Last update info (null if not defined) | + +**Status Codes:** + +- `200 OK`: Success +- `404 Not Found`: Topic does not exist +- `401 Unauthorized`: Missing or invalid auth token +- `403 Forbidden`: Insufficient permissions + +--- + +### 3. Create Topic + +**Purpose:** Create a new topic with configuration + +**Endpoint:** + +```http +POST /api/v1/admin/topics +``` + +**Request Body:** + +```json +{ + "topic_id": "purpose_discovery", + "topic_name": "Purpose Discovery Session", + "category": "purpose", + "topic_type": "conversation_coaching", + "description": "Discover your life's purpose through guided conversation", + "tier_level": "free", + "basic_model_code": "claude-3-5-sonnet-20241022", + "premium_model_code": "claude-3-5-sonnet-20241022", + "temperature": 0.7, + "max_tokens": 2000, + "top_p": 1.0, + "frequency_penalty": 0.0, + "presence_penalty": 0.0, + "is_active": false, + "display_order": 10, + "allowed_parameters": [ + { + "name": "user_name", + "type": "string", + "required": true, + "description": "User's display name" + }, + { + "name": "core_values", + "type": "string", + "required": false, + "description": "User's defined core values" + } + ] +} +``` + +**Allowed Parameter Types:** + +- `string`: Text value +- `integer`: Whole number +- `float`: Decimal number +- `boolean`: true/false +- `array`: List of values +- `object`: Nested structure + +**Parameter Definition Schema:** + +```json +{ + "name": "parameter_name", + "type": "string|integer|float|boolean|array|object", + "required": true, + "description": "Human-readable description" +} +``` + +**Validation Rules:** + +| Field | Rules | Allowed Values / Format | +|-------|-------|------------------------| +| `topic_id` | Required, unique, lowercase, snake_case, 3-50 chars | Regex: `^[a-z][a-z0-9_]*$` | +| `topic_name` | Required, 3-100 chars | Any printable characters | +| `category` | Required | Enum: `onboarding`, `conversation`, `insights`, `strategic_planning`, `operations_ai`, `operations_strategic_integration`, `analysis` | +| `topic_type` | Required | Enum: `conversation_coaching`, `single_shot`, `measure_system` | +| `tier_level` | Optional, default `free` | Enum: `free`, `basic`, `premium`, `ultimate` | +| `basic_model_code` | Required, must be valid model code | See "Supported Model Codes" below (used for FREE/BASIC tiers) | +| `premium_model_code` | Required, must be valid model code | See "Supported Model Codes" below (used for PREMIUM/ULTIMATE tiers) | +| `temperature` | Required, float | 0.0-2.0 | +| `max_tokens` | Required, integer | 1-100000 (model dependent) | +| `top_p` | Optional, float, default 1.0 | 0.0-1.0 | +| `frequency_penalty` | Optional, float, default 0.0 | -2.0 to 2.0 | +| `presence_penalty` | Optional, float, default 0.0 | -2.0 to 2.0 | +| `display_order` | Optional, integer, default 100 | 1-1000 | +| `description` | Optional | Max 500 chars | +| `is_active` | Optional, boolean, default false | `true`, `false` | + +**Supported Model Codes:** + +- `claude-3-5-sonnet-20241022` (recommended) +- `claude-3-5-haiku-20241022` +- `claude-3-opus-20240229` +- `claude-3-sonnet-20240229` +- `claude-3-haiku-20240307` +- `gpt-4o` +- `gpt-4-turbo` +- `gpt-4` +- `gpt-3.5-turbo` + +**Category Descriptions:** + +- `core_values`: Topics related to identifying and exploring personal values +- `purpose`: Life purpose and meaning discovery +- `vision`: Future vision and aspiration setting +- `goals`: Goal setting and achievement planning +- `strategy`: Strategic planning and decision making +- `measure`: Key performance indicators and metrics +- `custom`: Custom topics not fitting standard categories + +**Topic Type Descriptions:** + +- `conversation_coaching`: Interactive conversational coaching sessions (multi-turn) +- `single_shot`: One-shot evaluations, assessments, and analysis +- `measure_system`: Measure calculation and tracking + +**Prompt Types by Topic Type:** + +| Topic Type | Required Prompts | Description | +|------------|-----------------|-------------| +| `conversation_coaching` | `system`, `initiation`, `resume`, `extraction` | System defines coach behavior; initiation starts new sessions; resume continues paused sessions; extraction captures results | +| `single_shot` | `system`, `user` | System defines behavior; user template with parameters | +| `measure_system` | `system`, `user` | System defines calculation behavior; user template for input | + +**Response:** + +```json +{ + "topic_id": "purpose_discovery", + "created_at": "2024-11-13T16:00:00Z", + "message": "Topic created successfully. Upload prompts to activate." +} +``` + +**Status Codes:** + +- `201 Created`: Success +- `400 Bad Request`: Validation error +- `409 Conflict`: Topic ID already exists +- `401 Unauthorized`: Missing or invalid auth token +- `403 Forbidden`: Insufficient permissions + +**Error Response (Validation):** + +```json +{ + "error": "Validation failed", + "validation_errors": [ + { + "field": "topic_id", + "message": "Topic ID must be snake_case", + "code": "INVALID_FORMAT" + } + ] +} +``` + +--- + +### 4. Update Topic + +**Purpose:** Update topic configuration (excluding prompts) + +**Endpoint:** + +```http +PUT /api/v1/admin/topics/{topic_id} +``` + +**Path Parameters:** + +| Parameter | Type | Required | Description | Format | +|-----------|------|----------|-------------|--------| +| `topic_id` | string | Yes | Unique topic identifier | snake_case, 3-50 chars | + +**Request Body:** + +```json +{ + "topic_name": "Core Values - Updated Name", + "description": "Updated description", + "tier_level": "basic", + "basic_model_code": "claude-3-5-haiku-20241022", + "premium_model_code": "claude-3-5-sonnet-20241022", + "temperature": 0.5, + "max_tokens": 1500, + "is_active": true, + "display_order": 5, + "conversation_config": { + "max_messages_to_llm": 30, + "inactivity_timeout_minutes": 45, + "session_ttl_days": 14, + "estimated_messages": 25, + "extraction_model_code": "CLAUDE_3_5_SONNET_V2" + }, + "allowed_parameters": [ + { + "name": "user_name", + "type": "string", + "required": true, + "description": "User's display name" + } + ] +} +``` + +**Notes:** + +- Only include fields you want to update +- Cannot update `topic_id` +- Cannot update `category` or `topic_type` (create new topic instead) +- Cannot update `created_at` or `created_by` +- `allowed_parameters` replaces entire list when provided +- `conversation_config` is only applicable for `conversation_coaching` topic types + +**Response:** + +```json +{ + "topic_id": "core_values_coaching", + "updated_at": "2024-11-13T16:15:00Z", + "message": "Topic updated successfully" +} +``` + +**Status Codes:** + +- `200 OK`: Success +- `400 Bad Request`: Validation error +- `404 Not Found`: Topic does not exist +- `401 Unauthorized`: Missing or invalid auth token +- `403 Forbidden`: Insufficient permissions + +--- + +### 5. Delete Topic + +**Purpose:** Soft delete a topic (mark as inactive) + +**Endpoint:** + +```http +DELETE /api/v1/admin/topics/{topic_id} +``` + +**Query Parameters:** + +| Parameter | Type | Required | Default | Description | Allowed Values | +|-----------|------|----------|---------|-------------|----------------| +| `hard_delete` | boolean | No | false | If true, permanently delete (use with caution) | `true`, `false` | + +**Response:** + +```json +{ + "topic_id": "core_values_coaching", + "deleted_at": "2024-11-13T16:20:00Z", + "message": "Topic deactivated successfully" +} +``` + +**Status Codes:** + +- `200 OK`: Success (soft delete) +- `204 No Content`: Success (hard delete) +- `404 Not Found`: Topic does not exist +- `401 Unauthorized`: Missing or invalid auth token +- `403 Forbidden`: Insufficient permissions + +--- + +### 6. Get Prompt Content + +**Purpose:** Retrieve actual prompt content (markdown text) for editing + +**Endpoint:** + +```http +GET /api/v1/admin/topics/{topic_id}/prompts/{prompt_type} +``` + +**Path Parameters:** + +| Parameter | Type | Required | Description | Allowed Values | +|-----------|------|----------|-------------|----------------| +| `topic_id` | string | Yes | Unique topic identifier | snake_case, 3-50 chars | +| `prompt_type` | string | Yes | Type of prompt | Enum: `system`, `user`, `assistant` | + +**Response:** + +```json +{ + "topic_id": "core_values_coaching", + "prompt_type": "system", + "content": "You are an expert life coach specializing in helping people discover their core values...\n\n## Your Role\n...", + "s3_key": "prompts/core_values_coaching/system.md", + "updated_at": "2024-11-13T15:30:00Z", + "updated_by": "admin_123" +} +``` + +**Status Codes:** + +- `200 OK`: Success +- `404 Not Found`: Topic or prompt not found +- `401 Unauthorized`: Missing or invalid auth token +- `403 Forbidden`: Insufficient permissions + +--- + +### 7. Update Prompt Content + +**Purpose:** Update prompt markdown content + +**Endpoint:** + +```http +PUT /api/v1/admin/topics/{topic_id}/prompts/{prompt_type} +``` + +**Path Parameters:** + +| Parameter | Type | Required | Description | Allowed Values | +|-----------|------|----------|-------------|----------------| +| `topic_id` | string | Yes | Unique topic identifier | snake_case, 3-50 chars | +| `prompt_type` | string | Yes | Type of prompt | Enum: `system`, `user`, `assistant` | + +**Request Body:** + +```json +{ + "content": "# Updated System Prompt\n\nYou are an expert life coach...", + "commit_message": "Improved clarity and added examples" +} +``` + +**Validation:** + +- `content`: Required, markdown text, 1-50,000 chars, UTF-8 encoded +- `commit_message`: Optional, for audit trail, max 200 chars + +**Response:** + +```json +{ + "topic_id": "core_values_coaching", + "prompt_type": "system", + "s3_key": "prompts/core_values_coaching/system.md", + "updated_at": "2024-11-13T16:30:00Z", + "version": "1.2.0", + "message": "Prompt updated successfully" +} +``` + +**Status Codes:** + +- `200 OK`: Success +- `400 Bad Request`: Validation error +- `404 Not Found`: Topic not found +- `401 Unauthorized`: Missing or invalid auth token +- `403 Forbidden`: Insufficient permissions + +--- + +### 8. Create/Upload New Prompt + +**Purpose:** Add a new prompt type to a topic + +**Endpoint:** + +```http +POST /api/v1/admin/topics/{topic_id}/prompts +``` + +**Request Body:** + +```json +{ + "prompt_type": "assistant", + "content": "# Assistant Prompt\n\nProvide helpful guidance..." +} +``` + +**Validation:** + +- `prompt_type`: Required, enum: `system`, `user`, `assistant` +- `content`: Required, markdown text, 1-50,000 chars, UTF-8 encoded + +**Response:** + +```json +{ + "topic_id": "core_values_coaching", + "prompt_type": "assistant", + "s3_key": "prompts/core_values_coaching/assistant.md", + "created_at": "2024-11-13T16:35:00Z", + "message": "Prompt created successfully" +} +``` + +**Status Codes:** + +- `201 Created`: Success +- `400 Bad Request`: Validation error +- `409 Conflict`: Prompt type already exists +- `404 Not Found`: Topic not found +- `401 Unauthorized`: Missing or invalid auth token +- `403 Forbidden`: Insufficient permissions + +--- + +### 9. Delete Prompt + +**Purpose:** Remove a prompt from a topic + +**Endpoint:** + +```http +DELETE /api/v1/admin/topics/{topic_id}/prompts/{prompt_type} +``` + +**Response:** + +```json +{ + "message": "Prompt deleted successfully" +} +``` + +**Status Codes:** + +- `200 OK`: Success +- `404 Not Found`: Prompt not found +- `401 Unauthorized`: Missing or invalid auth token +- `403 Forbidden`: Insufficient permissions + +--- + +### 10. List Available Models + +**Purpose:** Get list of supported LLM models for topic configuration + +**Endpoint:** + +```http +GET /api/v1/admin/models +``` + +**Response:** + +```json +{ + "models": [ + { + "model_code": "claude-3-5-sonnet-20241022", + "model_name": "Claude 3.5 Sonnet", + "provider": "anthropic", + "capabilities": ["chat", "function_calling"], + "context_window": 200000, + "max_output_tokens": 4096, + "cost_per_input_million": 3.00, + "cost_per_output_million": 15.00, + "is_active": true + }, + { + "model_code": "claude-3-5-haiku-20241022", + "model_name": "Claude 3.5 Haiku", + "provider": "anthropic", + "capabilities": ["chat"], + "context_window": 200000, + "max_output_tokens": 4096, + "cost_per_input_million": 0.80, + "cost_per_output_million": 4.00, + "is_active": true + } + ] +} +``` + +**Status Codes:** + +- `200 OK`: Success +- `401 Unauthorized`: Missing or invalid auth token + +--- + +### 11. Validate Topic Configuration + +**Purpose:** Test a topic configuration before saving + +**Endpoint:** + +```http +POST /api/v1/admin/topics/validate +``` + +**Request Body:** + +```json +{ + "topic_id": "test_topic", + "topic_name": "Test Topic", + "category": "custom", + "topic_type": "single_shot", + "tier_level": "free", + "basic_model_code": "claude-3-5-sonnet-20241022", + "premium_model_code": "claude-3-5-sonnet-20241022", + "temperature": 0.7, + "max_tokens": 2000, + "prompts": [ + { + "prompt_type": "system", + "content": "Test system prompt with {user_name}" + } + ], + "allowed_parameters": [ + { + "name": "user_name", + "type": "string", + "required": true + } + ] +} +``` + +**Response (Valid):** + +```json +{ + "valid": true, + "warnings": [], + "suggestions": [ + "Consider lowering temperature to 0.5 for more consistent responses" + ] +} +``` + +**Response (Invalid):** + +```json +{ + "valid": false, + "errors": [ + { + "field": "prompts[0].content", + "message": "Prompt references parameter 'user_age' which is not defined", + "code": "UNDEFINED_PARAMETER" + } + ], + "warnings": [ + { + "field": "temperature", + "message": "High temperature may produce inconsistent results", + "code": "HIGH_TEMPERATURE" + } + ] +} +``` + +**Status Codes:** + +- `200 OK`: Validation complete (check `valid` field) +- `400 Bad Request`: Malformed request +- `401 Unauthorized`: Missing or invalid auth token + +--- + +### 12. Test Topic + +**Purpose:** Test a topic configuration by executing it with sample parameters. Allows admins to verify prompts work correctly before activating a topic. + +**Endpoint:** + +```http +POST /api/v1/admin/topics/{topic_id}/test +``` + +**Path Parameters:** + +| Parameter | Type | Required | Description | Format | +|-----------|------|----------|-------------|--------| +| `topic_id` | string | Yes | Unique topic identifier | snake_case, 3-50 chars | + +**Request Body:** + +```json +{ + "parameters": { + "website_url": "https://example.com", + "scan_depth": 2 + }, + "allow_inactive": false +} +``` + +**Notes:** + +- **Only supports `single_shot` topics** - conversation_coaching and other topic types cannot be tested via this endpoint +- Parameters not provided will be auto-enriched if a JWT token is supplied (template processor enabled) +- `allow_inactive`: When true, permits testing inactive topics for draft validation + +**Response (Success):** + +```json +{ + "success": true, + "topic_id": "website_scan", + "rendered_system_prompt": "...system prompt after substitutions...", + "rendered_user_prompt": "...user prompt after substitutions...", + "enriched_parameters": { + "website_url": "https://example.com", + "scan_depth": 2, + "website_content": "..." + }, + "response": { + "scan_id": "a1d3b5d8-42cd-4d76-80db-92cf3b4a1a91", + "captured_at": "2025-12-24T05:10:11Z", + "source_url": "https://example.com", + "company_profile": {"company_name": "Acme", "legal_name": "Acme, Inc.", "tagline": "...", "overview": "..."}, + "target_market": {"primary_audience": "...", "segments": ["..."], "pain_points": ["..."]}, + "offers": {"primary_product": "...", "categories": ["..."], "features": ["..."], "differentiators": ["..."]}, + "credibility": {"notable_clients": ["..."], "testimonials": [{"quote": "...", "attribution": "..."}]}, + "conversion": {"primary_cta_text": "...", "primary_cta_url": "https://example.com/demo", "supporting_assets": [{"label": "ROI calculator", "url": "https://example.com/roi"}]} + }, + "response_model": "WebsiteScanResponse", + "response_schema": {"title": "WebsiteScanResponse", "type": "object", "properties": {"scan_id": {"type": "string"}, "captured_at": {"type": "string"}}}, + "llm_metadata": { + "provider": "anthropic", + "model": "claude-3-5-sonnet-20241022", + "usage": {"prompt_tokens": 1200, "completion_tokens": 600, "total_tokens": 1800}, + "finish_reason": "stop" + }, + "execution_time_ms": 1245.5 +} +``` + +**Response (Error):** + +```json +{ + "success": false, + "topic_id": "website_scan", + "error": "Missing required parameters: website_url", + "execution_time_ms": 150.2 +} +``` + +**Status Codes:** + +- `200 OK`: Test completed (check `success` field) +- `400 Bad Request`: Unsupported topic type (only single_shot topics supported) +- `404 Not Found`: Topic not found +- `422 Unprocessable Entity`: Missing required parameters +- `401 Unauthorized`: Missing or invalid auth token +- `403 Forbidden`: Insufficient permissions +- `500 Internal Server Error`: AI processing or serialization failure + +--- + +### 14. Get Topic Usage Statistics (Planned) + +**Status:** ⏳ Not yet implemented + +**Purpose:** View usage metrics for a topic + +**Endpoint:** + +```http +GET /api/v1/admin/topics/{topic_id}/stats +``` + +**Query Parameters:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `start_date` | string | No | 30 days ago | ISO 8601 date | +| `end_date` | string | No | now | ISO 8601 date | + +**Response:** + +```json +{ + "topic_id": "core_values_coaching", + "period": { + "start": "2024-10-14T00:00:00Z", + "end": "2024-11-13T23:59:59Z" + }, + "usage": { + "total_conversations": 1247, + "total_tokens_used": 1850000, + "estimated_cost": 27.75 + } +} +``` + +--- + +## Admin UI Workflows + +### Configuring a Topic + +Topics are defined in the `endpoint_registry` code. Admins configure them by: + +1. **GET** `/api/v1/admin/topics` - View all topics with `templates` (showing allowed and defined prompts) +2. **GET** `/api/v1/admin/topics/{topic_id}` - Get topic details with `template_status` and `allowed_parameters` +3. **POST** `/api/v1/admin/topics/{topic_id}/prompts` - Upload required prompts (system, user, etc.) +4. **PUT** `/api/v1/admin/topics/{topic_id}` - Update model config (temperature, max_tokens, etc.) +5. **POST** `/api/v1/admin/topics/{topic_id}/test` - Test with sample parameters +6. **PUT** `/api/v1/admin/topics/{topic_id}` - Activate topic (`is_active: true`) + +### Editing Prompts + +1. **GET** `/api/v1/admin/topics/{topic_id}` - Get topic details with `template_status` and `allowed_parameters` +2. **GET** `/api/v1/admin/topics/{topic_id}/prompts/{prompt_type}` - Get current content +3. Edit in UI using `allowed_parameters` as available placeholders +4. **PUT** `/api/v1/admin/topics/{topic_id}/prompts/{prompt_type}` - Save changes +5. **POST** `/api/v1/admin/topics/{topic_id}/test` - Test the changes +6. Cache cleared automatically + +--- + +## Error Codes + +| Code | HTTP Status | Meaning | +|------|-------------|---------| +| `TOPIC_NOT_FOUND` | 404 | Topic ID does not exist | +| `TOPIC_EXISTS` | 409 | Topic ID already taken | +| `INVALID_TOPIC_ID` | 400 | Topic ID format invalid | +| `INVALID_MODEL` | 400 | Model code not recognized | +| `PROMPT_NOT_FOUND` | 404 | Prompt type not found | +| `PROMPT_EXISTS` | 409 | Prompt type already exists | +| `VALIDATION_ERROR` | 400 | Request validation failed | +| `UNAUTHORIZED` | 401 | Missing or invalid auth | +| `FORBIDDEN` | 403 | Insufficient permissions | +| `S3_ERROR` | 500 | Cloud storage error | +| `CACHE_ERROR` | 500 | Cache operation failed | + +--- + +## Rate Limiting + +Admin endpoints have separate rate limits: + +- **Read operations**: 100 requests/minute +- **Write operations**: 20 requests/minute +- **Bulk operations**: 5 requests/minute + +Headers: + +``` +X-RateLimit-Limit: 100 +X-RateLimit-Remaining: 95 +X-RateLimit-Reset: 1699987200 +``` + +--- + +## Permissions + +Required permission scopes: + +| Action | Permission | +|--------|-----------| +| List topics | `admin:topics:read` | +| View topic | `admin:topics:read` | +| Create topic | `admin:topics:write` | +| Update topic | `admin:topics:write` | +| Delete topic | `admin:topics:delete` | +| View prompts | `admin:topics:read` | +| Update prompts | `admin:prompts:write` | +| Test topic | `admin:topics:write` | +| View stats | `admin:topics:stats` | + +--- + +## Versioning + +API Version: `v1` + +Version in URL: `/api/v1/admin/...` + +Breaking changes will increment major version. + +--- + +## Support + +For API issues or questions: + +- Documentation: `llm_topic_architecture.md` +- Frontend changes: `fe_ai_specifications.md` +- Support: backend-team@purposepath.com diff --git a/docs/shared/Specifications/ai-user/backend-integration-unified-ai.md b/docs/shared/Specifications/ai-user/backend-integration-unified-ai.md new file mode 100644 index 00000000..106c7f50 --- /dev/null +++ b/docs/shared/Specifications/ai-user/backend-integration-unified-ai.md @@ -0,0 +1,3583 @@ +# Unified AI Endpoint Backend Integration Specifications + +**Version:** 2.6 +**Last Updated:** February 5, 2026 +**Service Base URL:** `{REACT_APP_COACHING_API_URL}` +**Default (Localhost):** `http://localhost:8000` +**Dev Environment:** `https://api.dev.purposepath.app/coaching/api/v1` + +[← Back to Index](./backend-integration-index.md) + +--- + +## Revision Log + +| Date | Version | Description | +|------|---------|-------------| +| 2026-02-05 | 2.6 | **Session Management Overhaul:** Changed `/start` to ALWAYS create new session (cancels existing). Added `/resume` endpoint with RESUME template for continuing sessions. Added `/session/check` endpoint for detecting existing sessions and conflicts. Idle sessions (>30min) are now resumable (not auto-abandoned). TTL set to 14 days for all sessions. | +| 2026-02-02 | 2.5 | **Insights Enhancement:** Enhanced insights_generation topic with KISS framework (Keep, Improve, Start, Stop), purpose-driven alignment analysis, measure-based state assessment, and leadership-focused framing. Now includes strategies and detailed measures data for comprehensive business analysis | +| 2026-01-29 | 2.4 | **Issue #201 Completion:** Redesigned website_scan response structure to align with BusinessFoundation data model - now extracts industry, founding year, vision/purpose hints, core values, and structures data for direct population of business foundation fields | +| 2026-01-29 | 2.3 | **Issue #200 Completion:** Enriched alignment_check topic with strategies parameter - alignment analysis now considers implementation strategies alongside goal and business foundation | +| 2026-01-25 | 2.2 | **Issue #196 Completion:** Fixed measure_recommendations field name (kpiName → name), verified all field names match Pydantic models, ensured prompt templates align with validation schemas | +| 2026-01-15 | 2.1 | Major restructure: Added revision log, index, reorganized topics by category, moved async and coaching endpoints to Core Endpoints section, verified all topics from registry | +| 2026-01-11 | 2.0 | Terminology update: Replaced all "KPI" references with "Measure", added new topics and parameter enrichment documentation | +| 2025-12-15 | 1.2 | Added async execution endpoints and coaching conversation sessions | +| 2025-11-01 | 1.1 | Added onboarding review topics | +| 2025-10-15 | 1.0 | Initial version with unified AI endpoint | + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Architecture](#architecture) +3. [Core Endpoints](#core-endpoints) + - [POST /ai/execute](#post-aiexecute) + - [GET /ai/topics](#get-aitopics) + - [GET /ai/schemas/{schema_name}](#get-aischemasschema_name) + - [POST /ai/execute-async](#post-aiexecute-async) + - [GET /ai/jobs/{jobId}](#get-aijobsjobid) + - [Coaching Conversation Endpoints](#coaching-conversation-endpoints) +4. [Available Parameters](#available-parameters) +5. [Supported Topics](#supported-topics) + - [Topic Summary by Category](#topic-summary-by-category) +6. [Single-Shot Topics by Category](#single-shot-topics-by-category) + - [Onboarding](#onboarding-topics) + - [Insights](#insights-topics) + - [Strategic Planning](#strategic-planning-topics) + - [Operations AI](#operations-ai-topics) + - [Operations-Strategic Integration](#operations-strategic-integration-topics) + - [Analysis](#analysis-topics) +7. [Conversation Topics](#conversation-topics) +8. [Response Model Schemas](#response-model-schemas) +9. [Error Handling](#error-handling) +10. [Usage Examples](#usage-examples) +11. [Changelog](#changelog) + +--- + +## Overview + +The Unified AI system is **topic-centric**, not endpoint-centric. All AI capabilities are organized as **topics** identified by a `topic_id`. Topics are routed to the appropriate endpoint based on their type: + +- **Single-Shot Topics** → `/ai/execute` or `/ai/execute-async` +- **Conversation Topics** → `/ai/coaching/*` endpoints + +Instead of separate endpoints for each capability, callers specify a `topic_id` and the appropriate parameters. The system handles routing, validation, and execution automatically. + +### Benefits + +- **Topic-driven architecture** - All capabilities defined as topics, not hardcoded endpoints +- **Single endpoint** for all single-shot AI operations (`/ai/execute`) +- **Dynamic response models** based on topic +- **Schema discovery** via `/ai/schemas/{schema_name}` +- **Topic listing** via `/ai/topics` +- **Consistent error handling** across all topics +- **Automatic parameter enrichment** from backend data sources +- **Flexible routing** - Endpoints determine routing based on `TopicType` + +### Frontend Implementation + +- **Primary Client:** `coachingClient` (axios instance in `src/services/api.ts`) +- **Key Pattern:** Call `/ai/execute` with `topic_id` and `parameters` +- **Topic Discovery:** Use `GET /ai/topics` to discover available topics and their parameters + +--- + +## Architecture + +### Topic-Centric Design + +The AI system is built around **topics**, not endpoints. Each topic represents a specific AI capability (e.g., `niche_review`, `alignment_check`, `core_values`). Topics are identified by a `topic_id` and are routed to the appropriate endpoint based on their `TopicType`: + +- **Single-Shot Topics** (`TopicType.SINGLE_SHOT`) → Use `/ai/execute` or `/ai/execute-async` +- **Conversation Topics** (`TopicType.CONVERSATION_COACHING`) → Use `/ai/coaching/*` endpoints + +**Key Principle:** Endpoints determine routing based on `TopicType`. Topic definitions do not include endpoint paths or HTTP methods—these are determined by the endpoint handlers. + +### Topic Data Sources + +Topic information comes from multiple sources, each serving a specific purpose: + +| Source | Purpose | Contains | +|--------|---------|----------| +| **Topic Registry** (Static Code) | Topic metadata and validation | `topic_id`, `topic_type`, `category`, `description`, `response_model`, `parameter_refs`, `is_active` | +| **DynamoDB** (via `TopicRepository`) | Runtime topic configuration | `model_code`, `temperature`, `max_tokens`, execution parameters, runtime settings | +| **S3** (via `S3PromptStorage`) | Template content | System prompts, user prompts, template files referenced by topic | +| **Topic Seed Data** | Initialization | Default configurations used to seed/initialize topics in DynamoDB | + +**Execution Flow:** +1. Request arrives with `topic_id` → Topic Registry validates topic exists and is active +2. DynamoDB provides runtime configuration (model, temperature, etc.) +3. S3 provides prompt templates +4. Parameter enrichment fetches data from various sources +5. LLM executes with configured model and prompts +6. Response is serialized to the topic's response model + +### Parameter Enrichment System + +The AI backend automatically enriches prompts with data from multiple sources. When you call `/ai/execute`, the system: + +1. **Identifies required parameters** from the topic registry +2. **Groups parameters by source** (one API call per source) +3. **Fetches data** from Business API, Account Service, etc. +4. **Extracts individual values** using defined extraction paths +5. **Renders prompts** with all gathered parameters + +This means frontend only needs to provide **request-level parameters** (e.g., `goal_id`, `measure_id`). Context like business foundation, strategies, and measures is automatically fetched. + +### Parameter Sources + +| Source | Description | Example Parameters | +|--------|-------------|-------------------| +| `REQUEST` | Provided in API request body | `goal_id`, `measure_id`, `current_value` | +| `ONBOARDING` | Business foundation from Account Service | `vision`, `purpose`, `icas`, `core_values` | +| `GOAL` | Single goal from Traction Service | `goal`, `goal_name`, `goal_description` | +| `GOALS` | All goals from Traction Service | `goals`, `goals_count` | +| `MEASURE` | Single measure from Traction Service | `measure`, `measure_name`, `measure_unit` | +| `MEASURES` | All measures/summary from Traction Service | `measures`, `measures_count` | +| `ACTION` | Single action from Traction Service | `action`, `action_title`, `action_status` | +| `ISSUE` | Single issue from Traction Service | `issue`, `issue_title`, `issue_priority` | +| `WEBSITE` | Scraped website content | `website_content`, `website_title` | +| `CONVERSATION` | Current conversation context | `conversation_history` | +| `COMPUTED` | Derived from other parameters | `alignment_score` | + +--- + +## Core Endpoints + +**Note:** All endpoints use topic-based routing. Topics are identified by `topic_id` and routed based on their `TopicType`: +- `TopicType.SINGLE_SHOT` → `/ai/execute` or `/ai/execute-async` +- `TopicType.CONVERSATION_COACHING` → `/ai/coaching/*` endpoints + +Topic definitions do not include endpoint paths or HTTP methods—these are determined by the endpoint handlers based on the topic's type. + +--- + +### Standard Response Envelope + +**CRITICAL:** All API responses (coaching, single-shot, admin, health, etc.) are wrapped in a standard envelope structure. This is consistent across **ALL endpoints**. + +**Success Response Structure:** + +```json +{ + "success": true, + "data": { + // Endpoint-specific response data (varies by endpoint) + }, + "message": "Operation completed successfully", // Optional success message + "error": null, + "error_code": null, + "request_id": "550e8400-e29b-41d4-a716-446655440000", // UUID for request tracing + "timestamp": "2026-02-08T21:45:23.123456Z" // ISO 8601 timestamp with microseconds +} +``` + +**Error Response Structure:** + +```json +{ + "success": false, + "data": null, + "message": null, + "error": "Detailed error message here", + "error_code": "ERROR_CODE_HERE", // Optional structured error code + "request_id": "550e8400-e29b-41d4-a716-446655440000", + "timestamp": "2026-02-08T21:45:23.123456Z" +} +``` + +**Envelope Fields:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `success` | boolean | Yes | `true` for successful responses, `false` for errors | +| `data` | object \| null | Yes | Response payload (endpoint-specific), `null` on error | +| `message` | string \| null | No | Optional success or info message | +| `error` | string \| null | No | Error message (only present when `success: false`) | +| `error_code` | string \| null | No | Structured error code for programmatic error handling | +| `request_id` | string | Yes | Unique UUID for request tracing and debugging | +| `timestamp` | string | Yes | ISO 8601 timestamp with microseconds (UTC) | + +**Important Notes:** +- **All endpoints** use this envelope - there are no exceptions +- Frontend should always check the `success` field first +- Use `request_id` for debugging and error reporting +- The `data` field structure varies by endpoint (documented in each endpoint section below) +- When `success: false`, `data` is always `null` and `error` contains the error message + +--- + +### POST /ai/execute + +Execute any registered single-shot AI topic. + +**Routing:** This endpoint handles all topics with `TopicType.SINGLE_SHOT`. + +**Full URL:** `{BASE_URL}/ai/execute` + +**Request:** + +```json +{ + "topic_id": "string", + "parameters": { + // Topic-specific parameters (request-level only) + } +} +``` + +**Response:** + +```json +{ + "topic_id": "string", + "success": true, + "data": { + // Response varies by topic - see schema_ref + }, + "schema_ref": "string", + "metadata": { + "model": "string", + "tokens_used": 0, + "processing_time_ms": 0, + "finish_reason": "stop" + } +} +``` + +**Error Responses:** + +| Status | Reason | +|--------|--------| +| 404 | Topic not found in topic registry | +| 400 | Topic is not active (inactive in registry or DynamoDB) | +| 400 | Topic is conversation type (use `/ai/coaching/*` endpoints) | +| 422 | Missing required parameters | +| 500 | Response model not configured | + +--- + +### GET /ai/topics + +List all available single-shot topics with their parameters. + +**Full URL:** `{BASE_URL}/ai/topics` + +**Response:** + +```json +[ + { + "topic_id": "niche_review", + "description": "Review and suggest variations for business niche", + "response_model": "OnboardingReviewResponse", + "parameters": [ + { + "name": "current_value", + "type": "string", + "required": false, + "description": "Current niche value to review" + } + ] + } +] +``` + +--- + +### GET /ai/schemas/{schema_name} + +Get JSON schema for a response model. + +**Full URL:** `{BASE_URL}/ai/schemas/{schema_name}` + +**Example:** `GET /ai/schemas/OnboardingReviewResponse` + +**Response:** JSON Schema definition for the response model. + +--- + +### POST /ai/execute-async + +Execute an AI topic asynchronously. Returns immediately with a job ID; results delivered via WebSocket. + +**Routing:** This endpoint handles all topics with `TopicType.SINGLE_SHOT` (same as `/ai/execute`, but for long-running operations). + +**Full URL:** `{BASE_URL}/ai/execute-async` + +**Request:** Same as `/ai/execute` + +```json +{ + "topic_id": "niche_review", + "parameters": { + "current_value": "We help small business owners with marketing" + } +} +``` + +**Response (Immediate):** + +```json +{ + "success": true, + "data": { + "jobId": "550e8400-e29b-41d4-a716-446655440000", + "status": "pending", + "topicId": "niche_review", + "estimatedDurationMs": 30000 + } +} +``` + +**Job Statuses:** + +- `pending` - Job accepted, queued for processing +- `processing` - Job is actively being processed +- `completed` - Job finished successfully (result in WebSocket event) +- `failed` - Job failed (error in WebSocket event) + +--- + +### GET /ai/jobs/{jobId} + +Check status of an async job. Use for polling fallback if WebSocket disconnects. + +**Full URL:** `{BASE_URL}/ai/jobs/{jobId}` + +**Response (Completed):** + +```json +{ + "success": true, + "data": { + "jobId": "550e8400-e29b-41d4-a716-446655440000", + "status": "completed", + "topicId": "niche_review", + "createdAt": "2026-01-11T20:00:00Z", + "completedAt": "2026-01-11T20:00:35Z", + "result": { + "qualityReview": "...", + "suggestions": [...] + }, + "processingTimeMs": 35000 + } +} +``` + +**Response (Failed):** + +```json +{ + "success": true, + "data": { + "jobId": "550e8400-e29b-41d4-a716-446655440000", + "status": "failed", + "topicId": "niche_review", + "createdAt": "2026-01-11T20:00:00Z", + "completedAt": "2026-01-11T20:00:45Z", + "error": "LLM provider timeout", + "errorCode": "LLM_TIMEOUT" + } +} +``` + +### WebSocket Events + +Results are delivered via the existing WebSocket connection at `wss://{WEBSOCKET_URL}`. + +#### ai.job.completed + +```json +{ + "jobId": "550e8400-e29b-41d4-a716-446655440000", + "tenantId": "tenant-123", + "userId": "user-456", + "topicId": "niche_review", + "eventType": "ai.job.completed", + "data": { + "jobId": "550e8400-e29b-41d4-a716-446655440000", + "topicId": "niche_review", + "result": { + "topic_id": "niche_review", + "success": true, + "data": { + "qualityReview": "...", + "suggestions": [...] + }, + "schema_ref": "OnboardingReviewResponse" + }, + "processingTimeMs": 35000 + }, + "stage": "dev" +} +``` + +**Key Fields:** +- `jobId`, `tenantId`, `userId`, `topicId`, `eventType` - Top-level metadata +- `data.result` - The actual AI response (structure varies by topic) +- `data.processingTimeMs` - How long the AI took to process +- `stage` - Environment (dev/staging/prod) + +#### ai.job.failed + +```json +{ + "jobId": "550e8400-e29b-41d4-a716-446655440000", + "tenantId": "tenant-123", + "userId": "user-456", + "topicId": "niche_review", + "eventType": "ai.job.failed", + "data": { + "jobId": "550e8400-e29b-41d4-a716-446655440000", + "topicId": "niche_review", + "error": "LLM provider timeout after 60 seconds", + "errorCode": "LLM_TIMEOUT" + }, + "stage": "dev" +} +``` + +### When to Use Async vs Sync + +| Use Case | Endpoint | Reason | +|----------|----------|--------| +| Quick operations (<10s) | `POST /ai/execute` | Simpler, immediate response | +| Complex analysis (>20s) | `POST /ai/execute-async` | Avoids API Gateway timeout | +| Batch operations | `POST /ai/execute-async` | Process in background | +| User waiting on screen | `POST /ai/execute-async` | Better UX with progress | + +**Recommended:** Use async for onboarding review topics (`niche_review`, `ica_review`, `value_proposition_review`) as they may take 30-60 seconds depending on LLM load. + +--- + +### Coaching Conversation Endpoints + +The Coaching Conversation endpoints provide multi-turn conversational coaching interactions. Unlike single-shot `/ai/execute` endpoints, coaching sessions maintain state across multiple messages. + +**Routing:** These endpoints handle all topics with `TopicType.CONVERSATION_COACHING`. + +**Base URL:** All coaching conversation endpoints are prefixed with `/ai/coaching`. + +**Full URL Pattern:** `{BASE_URL}/ai/coaching/{endpoint}` + +#### Available Coaching Topics + +| topic_id | Name | Description | +|----------|------|-------------| +| `core_values` | Core Values Discovery | Discover and articulate your organization's authentic core values | +| `purpose` | Purpose Discovery | Define your organization's deeper purpose and reason for existing | +| `vision` | Vision Crafting | Craft a compelling vision for your organization's future | + +**Important:** When calling `/ai/coaching/start`, use the `topic_id` value (e.g., `"core_values"`, `"purpose"`, `"vision"`) - NOT the registry key format. + +#### GET /ai/coaching/topics + +Get all coaching topics with user's completion status. + +**Full URL:** `{BASE_URL}/ai/coaching/topics` + +**Response:** + +```json +{ + "success": true, + "data": { + "topics": [ + { + "topic_id": "core_values", + "name": "Core Values Discovery", + "description": "Discover and articulate your organization's authentic core values", + "status": "not_started", + "session_id": null, + "completed_at": null + } + ] + }, + "message": "Topics retrieved successfully" +} +``` + +#### GET /ai/coaching/session/check + +**NEW in v2.6** - Check if a resumable session exists for a topic. + +**Full URL:** `{BASE_URL}/ai/coaching/session/check?topic_id={topic_id}` + +**Request Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `topic_id` | string | Yes | ID of the coaching topic to check | + +**Response:** + +```json +{ + "success": true, + "data": { + "has_session": true, + "session_id": "sess_abc123", + "status": "paused", + "actual_status": "active", + "is_idle": true, + "conflict": false, + "conflict_user_id": null + } +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `has_session` | boolean | Whether current user has an active/paused session for this topic | +| `session_id` | string \| null | Session ID if exists | +| `status` | string \| null | Computed status: "active" or "paused" (see below) | +| `actual_status` | string \| null | Raw database status | +| `is_idle` | boolean | Whether session has been idle > 30 minutes | +| `conflict` | boolean | Whether another user from same tenant has active session | +| `conflict_user_id` | string \| null | Other user's ID if conflict | + +**Status Logic:** +- Returns `"paused"` if session is explicitly PAUSED **OR** ACTIVE but idle > 30 minutes +- Returns `"active"` if session is ACTIVE and NOT idle + +**Use Case:** Frontend calls this before starting/resuming to show appropriate UI: +- If `status === "paused"` → Show "Resume or Start New?" dialog +- If `conflict === true` → Show "Another user is using this topic" +- If `has_session === false` → Show "Start Session" + +--- + +#### POST /ai/coaching/start + +**CHANGED in v2.6** - Now ALWAYS creates a new session (cancels any existing session). + +Start a brand new coaching session. If user has an existing session for this topic, +it will be cancelled/abandoned first. Use `/resume` endpoint to continue existing sessions. + +**Full URL:** `{BASE_URL}/ai/coaching/start` + +**Request:** + +```json +{ + "topic_id": "core_values", + "context": { + "business_name": "Acme Corp", + "industry": "Technology" + } +} +``` + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `topic_id` | string | Yes | ID of the coaching topic | +| `context` | object | No | Optional context data for the session | + +**Response:** + +```json +{ + "success": true, + "data": { + "session_id": "sess_new456", + "tenant_id": "tenant-123", + "topic_id": "core_values", + "status": "active", + "message": "Welcome! Let's begin exploring your core values...", + "turn": 1, + "max_turns": 10, + "is_final": false, + "resumed": false, + "metadata": { + "model": "us.anthropic.claude-3-5-sonnet-20241022-v2:0", + "processing_time_ms": 1250, + "tokens_used": 150 + } + }, + "message": "Session started successfully", + "error": null, + "error_code": null, + "request_id": "550e8400-e29b-41d4-a716-446655440000", + "timestamp": "2026-02-08T21:45:23.123456Z" +} +``` + +**Note:** Uses INITIATION template for fresh start conversation. + +--- + +#### POST /ai/coaching/resume + +**NEW in v2.6** - Resume an existing coaching session with welcome-back message. + +Continue an existing session using the RESUME template, which welcomes the user back +and summarizes the conversation so far. Works for both PAUSED and ACTIVE sessions. + +**Full URL:** `{BASE_URL}/ai/coaching/resume` + +**Request:** + +```json +{ + "session_id": "sess_abc123" +} +``` + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `session_id` | string | Yes | ID of the session to resume | + +**Response:** + +```json +{ + "success": true, + "data": { + "session_id": "sess_abc123", + "tenant_id": "tenant-123", + "topic_id": "core_values", + "status": "active", + "message": "Welcome back! Last time we discussed your core values: integrity and innovation. Let's continue...", + "turn": 3, + "max_turns": 10, + "is_final": false, + "resumed": true, + "metadata": { + "model": "us.anthropic.claude-3-5-sonnet-20241022-v2:0", + "processing_time_ms": 1350, + "tokens_used": 180 + } + }, + "message": "Session resumed successfully", + "error": null, + "error_code": null, + "request_id": "550e8400-e29b-41d4-a716-446655440000", + "timestamp": "2026-02-08T21:45:23.123456Z" +} +``` + +**Note:** Uses RESUME template with conversation summary. If session was PAUSED, status changes to ACTIVE. + +#### POST /ai/coaching/message + +Send a message in an active coaching session using async execution. + +**CHANGED in v2.7:** +- Endpoint returns `202 Accepted` immediately with a `job_id` +- AI response is delivered asynchronously (WebSocket event or polling endpoint) +- Polling response now includes `turn`, `max_turns`, and `message_count` + +**Full URL:** `{BASE_URL}/ai/coaching/message` + +**Request:** + +```json +{ + "session_id": "sess_abc123", + "message": "I think integrity and innovation are most important to me" +} +``` + +**Response (202 Accepted):** + +```json +{ + "success": true, + "data": { + "job_id": "5517f5a7-8886-4d83-a083-add50c663dc6", + "session_id": "sess_abc123", + "status": "pending", + "estimated_duration_ms": 45000 + }, + "message": "Message job created, processing asynchronously", + "error": null, + "error_code": null, + "request_id": "550e8400-e29b-41d4-a716-446655440000", + "timestamp": "2026-02-08T21:45:23.123456Z" +} +``` + +**Polling endpoint:** `GET /ai/coaching/message/{job_id}` + +```json +{ + "success": true, + "data": { + "job_id": "5517f5a7-8886-4d83-a083-add50c663dc6", + "session_id": "sess_abc123", + "status": "completed", + "message": "That's wonderful! Can you tell me more about how integrity shows up in your daily business decisions?", + "is_final": false, + "result": null, + "turn": 3, + "max_turns": 10, + "message_count": 6, + "error": null, + "processing_time_ms": 1234 + }, + "message": "Job status: completed", + "error": null, + "error_code": null, + "request_id": "550e8400-e29b-41d4-a716-446655440000", + "timestamp": "2026-02-08T21:45:23.123456Z" +} +``` + +**WebSocket completion event (`ai.message.completed`)**: + +```json +{ + "eventType": "ai.message.completed", + "jobId": "5517f5a7-8886-4d83-a083-add50c663dc6", + "sessionId": "sess_abc123", + "tenantId": "tenant-123", + "userId": "user-123", + "data": { + "message": "Thank you for this wonderful conversation! I've captured your core values...", + "isFinal": true, + "turn": 8, + "maxTurns": 10, + "messageCount": 16, + "result": { + "core_values": ["Integrity", "Innovation", "Collaboration"], + "value_descriptions": { + "Integrity": "Doing the right thing even when no one is watching", + "Innovation": "Constantly seeking better ways to serve our customers", + "Collaboration": "Working together to achieve shared goals" + } + } + } +} +``` + +**Event source of truth:** see `docs/shared/Specifications/eventbridge/async-coaching-message-events.md` + +#### POST /ai/coaching/pause + +Pause an active coaching session. + +**Full URL:** `{BASE_URL}/ai/coaching/pause` + +**Request:** + +```json +{ + "session_id": "sess_abc123" +} +``` + +#### POST /ai/coaching/complete + +Complete a coaching session and extract results. + +**Full URL:** `{BASE_URL}/ai/coaching/complete` + +**Request:** + +```json +{ + "session_id": "sess_abc123" +} +``` + +#### POST /ai/coaching/cancel + +Cancel a coaching session. + +**Full URL:** `{BASE_URL}/ai/coaching/cancel` + +#### GET /ai/coaching/session + +Get detailed information about a coaching session. + +**Full URL:** `{BASE_URL}/ai/coaching/session?session_id={session_id}` + +#### GET /ai/coaching/sessions + +List all coaching sessions for the current user. + +**Full URL:** `{BASE_URL}/ai/coaching/sessions?include_completed={bool}&limit={int}` + +--- + +## Available Parameters + +Parameters can be provided in the request body or automatically enriched from various sources. The following parameter sources are available: + +| Source | Description | Example Parameters | +|--------|-------------|-------------------| +| `REQUEST` | Provided in API request body | `goal_id`, `measure_id`, `current_value` | +| `ONBOARDING` | Business foundation from Account Service | `vision`, `purpose`, `icas`, `core_values` | +| `GOAL` | Single goal from Traction Service | `goal`, `goal_name`, `goal_description` | +| `GOALS` | All goals from Traction Service | `goals`, `goals_count` | +| `MEASURE` | Single measure from Traction Service | `measure`, `measure_name`, `measure_unit` | +| `MEASURES` | All measures/summary from Traction Service | `measures`, `measures_count` | +| `ACTION` | Single action from Traction Service | `action`, `action_title`, `action_status` | +| `ISSUE` | Single issue from Traction Service | `issue`, `issue_title`, `issue_priority` | +| `WEBSITE` | Scraped website content | `website_content`, `website_title` | +| `CONVERSATION` | Current conversation context | `conversation_history` | +| `COMPUTED` | Derived from other parameters | `alignment_score` | + +### Business Foundation Parameters + +These parameters are automatically fetched from the Account Service: + +| Parameter | Type | Description | +|-----------|------|-------------| +| `vision` | string | Organization's vision statement | +| `purpose` | string | Organization's purpose statement | +| `icas` | array | Ideal Client Avatars with demographics, pain points, goals | +| `core_values` | array | List of core value objects with name, description | +| `pillars` | array | Business foundation pillars | +| `industry` | string | Industry classification | +| `business_type` | string | Type of business | +| `business_stage` | string | Current business stage | + +### Strategy Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `strategy` | object | Single strategy details | +| `strategy_name` | string | Strategy name | +| `strategy_description` | string | Strategy description | +| `strategy_alignment_score` | integer | Alignment with business foundation | +| `strategies` | array | All strategies for tenant | + +### Goal Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `goal` | object | Complete goal data | +| `goal_name` | string | Goal name/title | +| `goal_description` | string | Goal description | +| `goal_status` | string | Current status | +| `goal_progress` | integer | Progress percentage (0-100) | +| `goal_due_date` | string | Target completion date | +| `goals` | array | All goals for user | +| `goals_count` | integer | Total number of goals | + +### Measure Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `measure` | object | Complete measure data | +| `measure_name` | string | Measure name | +| `measure_description` | string | Measure description | +| `measure_unit` | string | Unit of measurement | +| `measure_direction` | string | Target direction (up/down/maintain) | +| `measure_type` | string | Measure type | +| `measure_current_value` | number | Current value | +| `measures` | array | All measures for tenant | +| `measures_count` | integer | Total number of measures | +| `measures_summary` | object | Aggregated measures summary | + +### Action Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `action` | object | Complete action data | +| `action_title` | string | Action title | +| `action_description` | string | Action description | +| `action_status` | string | Current status | +| `action_priority` | string | Priority level | +| `action_due_date` | string | Due date | +| `actions` | array | All actions | + +### Issue Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `issue` | object | Complete issue data | +| `issue_title` | string | Issue title | +| `issue_description` | string | Issue description | +| `issue_status` | string | Current status | +| `issue_priority` | string | Priority level | +| `issues` | array | All issues | + +### People & Organization Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `people` | array | All people in organization | +| `person` | object | Single person details | +| `departments` | array | Organization departments | +| `positions` | array | Organization positions | + +--- + +## Supported Topics + +### Topic Summary by Category + +#### Onboarding + +- [website_scan](#topic-website_scan) - Scan a website and extract business information +- [onboarding_suggestions](#topic-onboarding_suggestions) - Generate onboarding suggestions based on scanned website +- [onboarding_coaching](#topic-onboarding_coaching) - AI coaching for onboarding process +- [business_metrics](#topic-business_metrics) - Retrieve business metrics and data for coaching context +- [niche_review](#topic-niche_review) - Review and suggest variations for business niche +- [ica_review](#topic-ica_review) - Review and suggest detailed ICA personas with demographics, goals, pain points, and buying behavior +- [value_proposition_review](#topic-value_proposition_review) - Review and suggest variations for value proposition +- [core_values](#topic-core_values) - Discover and articulate your organization's authentic core values through guided coaching +- [purpose](#topic-purpose) - Define your organization's deeper purpose and reason for existing through guided coaching +- [vision](#topic-vision) - Craft a compelling vision for your organization's future through guided coaching + +#### Insights + +- [insights_generation](#topic-insights_generation) - Generate leadership insights using KISS framework with purpose-driven alignment analysis + +#### Strategic Planning + +- [goal_intent_review](#topic-goal_intent_review) - Review and suggest goal intent statements (WHAT + WHY) +- [strategy_suggestions](#topic-strategy_suggestions) - Generate strategic planning suggestions +- [measure_recommendations](#topic-measure_recommendations) - Recommend measures based on business goals +- [alignment_check](#topic-alignment_check) - Calculate alignment score between goal and business foundation +- [alignment_explanation](#topic-alignment_explanation) - Explain alignment score calculation +- [alignment_suggestions](#topic-alignment_suggestions) - Suggest improvements to increase alignment + +#### Operations AI + +- [root_cause_suggestions](#topic-root_cause_suggestions) - Suggest root causes for operational issues +- [swot_analysis](#topic-swot_analysis) - Generate SWOT analysis for operations +- [five_whys_questions](#topic-five_whys_questions) - Generate Five Whys analysis questions +- [action_suggestions](#topic-action_suggestions) - Suggest actions to resolve operational issues +- [optimize_action_plan](#topic-optimize_action_plan) - Optimize action plan for better execution +- [prioritization_suggestions](#topic-prioritization_suggestions) - Suggest prioritization of operational tasks +- [scheduling_suggestions](#topic-scheduling_suggestions) - Suggest optimal scheduling for tasks +- [categorize_issue](#topic-categorize_issue) - Categorize operational issue by type and severity +- [assess_impact](#topic-assess_impact) - Assess business impact of operational issue + +#### Operations-Strategic Integration + +- [action_strategic_context](#topic-action_strategic_context) - Get strategic context for a specific action +- [suggest_connections](#topic-suggest_connections) - Suggest strategic connections for actions +- [update_connections](#topic-update_connections) - Update strategic connections for an action +- [create_issue_from_action](#topic-create_issue_from_action) - Create an issue from an incomplete action +- [create_action_from_issue](#topic-create_action_from_issue) - Create action items from an issue +- [complete_action](#topic-complete_action) - Complete an action and update related items +- [close_issue](#topic-close_issue) - Close an issue and update related items +- [issue_status](#topic-issue_status) - Get comprehensive status of an issue +- [issue_related_actions](#topic-issue_related_actions) - Get actions related to an issue +- [update_measure](#topic-update_measure) - Update a measure value with audit trail +- [calculate_measure](#topic-calculate_measure) - Calculate measure value from linked data +- [measure_history](#topic-measure_history) - Get historical values for a measure +- [measure_impact](#topic-measure_impact) - Analyze measure impact on strategic goals +- [action_measure_impact](#topic-action_measure_impact) - Calculate impact of action completion on measures +- [sync_measures_to_strategy](#topic-sync_measures_to_strategy) - Sync operational measures to strategic planning +- [detect_measure_conflicts](#topic-detect_measure_conflicts) - Detect conflicts between operational and strategic measures +- [resolve_measure_conflict](#topic-resolve_measure_conflict) - Resolve a detected measure conflict +- [operations_strategic_alignment](#topic-operations_strategic_alignment) - Get alignment status between operations and strategy +- [cascade_action_update](#topic-cascade_action_update) - Cascade action updates to related items +- [cascade_issue_update](#topic-cascade_issue_update) - Cascade issue updates to related items +- [cascade_measure_update](#topic-cascade_measure_update) - Cascade measure updates to related strategic items + +#### Analysis + +- [topic_strategic_context](#topic-topic_strategic_context) - Get strategic context for admin topic management +- [alignment_analysis](#topic-alignment_analysis) - Analyze goal alignment with business foundation +- [measure_analysis](#topic-measure_analysis) - Analyze measure performance trends +- [operations_analysis](#topic-operations_analysis) - Perform operational analysis (SWOT, root cause, etc.) + +--- + +## Single-Shot Topics by Category + +### Onboarding Topics + +#### Topic: `website_scan` + +Scan a website URL and extract business foundation information for onboarding. The response structure aligns with the BusinessFoundation data model to facilitate direct population of business foundation fields. + +**Request Payload Structure:** + +```json +{ + "topic_id": "website_scan", + "parameters": { + "website_url": "https://example.com" + } +} +``` + +**Request Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `website_url` | string | Yes | URL of website to scan (must start with http:// or https://) | + +**Response Model:** `WebsiteScanResponse` — [Get JSON Schema](/ai/schemas/WebsiteScanResponse) + +```json +{ + "topic_id": "website_scan", + "success": true, + "data": { + "scan_id": "scan_f3c9ab", + "captured_at": "2026-01-29T18:45:00Z", + "source_url": "https://example.com", + "business_profile": { + "business_name": "Example Corp", + "business_description": "Example Corp provides AI-powered strategic planning tools that help growing businesses achieve clarity and focus.", + "industry": "Technology", + "year_founded": 2018, + "headquarters_location": "San Francisco, CA", + "website": "https://example.com" + }, + "core_identity": { + "vision_hint": "To be the leading platform for strategic business planning", + "purpose_hint": "We empower businesses to achieve their full potential through clarity and focus", + "inferred_values": ["Innovation", "Integrity", "Customer Success", "Continuous Improvement"] + }, + "target_market": { + "niche_statement": "Mid-market B2B SaaS companies seeking strategic planning and execution tools", + "segments": ["B2B SaaS (ARR $5M-$50M)", "Growth-stage technology companies", "Professional services firms"], + "pain_points": ["Fragmented strategic planning tools", "Lack of goal-execution alignment", "Difficulty measuring progress"] + }, + "products": [ + { + "name": "PurposePath Pro", + "description": "AI-powered strategic planning platform with goal setting, measure tracking, and team alignment", + "problem_solved": "Businesses struggle to translate vision into actionable plans and measure progress effectively", + "key_features": ["Goal wizard", "Measure tracking", "AI recommendations", "Team collaboration"] + }, + { + "name": "Executive Coaching", + "description": "One-on-one strategic coaching for business leaders", + "problem_solved": "Leaders need guidance to develop effective strategies and overcome execution challenges", + "key_features": ["Personalized coaching", "Strategic frameworks", "Accountability system"] + } + ], + "value_proposition": { + "unique_selling_proposition": "The only strategic planning platform that combines AI insights with proven business frameworks", + "key_differentiators": ["AI-powered recommendations", "Integrated coaching", "Built specifically for strategic planning"], + "proof_points": ["500+ businesses served", "78% goal completion rate", "92% report better strategic clarity"] + } + }, + "schema_ref": "WebsiteScanResponse", + "metadata": { + "model": "us.anthropic.claude-3-5-sonnet-20241022-v2:0", + "tokens_used": 2100, + "processing_time_ms": 9200, + "finish_reason": "stop" + } +} +``` + +**Response Structure Details:** + +| Section | Fields | Description | +|---------|--------|-------------| +| `business_profile` | business_name, business_description, industry, year_founded, headquarters_location, website | Core business information aligned with BusinessFoundation.profile | +| `core_identity` | vision_hint, purpose_hint, inferred_values | Vision, purpose, and values inferred from website content, aligned with BusinessFoundation.identity | +| `target_market` | niche_statement, segments, pain_points | Target market information aligned with BusinessFoundation.market | +| `products` | name, description, problem_solved, key_features | Product/service array aligned with BusinessFoundation.products | +| `value_proposition` | unique_selling_proposition, key_differentiators, proof_points | Value proposition aligned with BusinessFoundation.proposition | + +**Notes:** + +- This topic uses a **retrieval method** (`get_website_content`) to fetch and parse the website +- The `website_url` parameter is passed from the frontend payload +- The retrieval method scrapes the website and provides `website_content`, `website_title`, and `meta_description` to the prompt template +- **Response structure aligns with BusinessFoundation data model** - extracted data can be directly mapped to business foundation fields +- Optional fields (industry, year_founded, vision_hint, purpose_hint, etc.) return null if not found on website +- May return partial results if website has anti-scraping measures +- Use extracted data to pre-populate business foundation onboarding wizard + +--- + +#### Topic: `onboarding_suggestions` + +Generate onboarding suggestions based on scanned website. + +**Request Payload Structure:** + +```json +{ + "topic_id": "onboarding_suggestions", + "parameters": { + "kind": "niche", + "current": "We help small businesses grow", + "context": { + "industry": "Software", + "targetMarket": "SMBs" + } + } +} +``` + +**Request Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `kind` | string | Yes | Type of suggestion: "niche", "ica", or "valueProposition" | +| `current` | string | No | Current draft text (optional) | +| `context` | object | No | Business context dictionary (key-value pairs) | + +**Response Model:** `OnboardingSuggestionsResponse` + +**Response Payload Structure:** + +```json +{ + "suggestions": ["string"], + "reasoning": "string" +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `suggestions` | array of string | AI-generated suggestions | +| `reasoning` | string | Explanation of why these suggestions fit | + +--- + +#### Topic: `onboarding_coaching` + +AI coaching for onboarding process. + +**Request Payload Structure:** + +```json +{ + "topic_id": "onboarding_coaching", + "parameters": { + "topic": "coreValues", + "message": "I'm struggling to identify our core values", + "context": { + "stage": "early", + "priorAttempts": 2 + } + } +} +``` + +**Request Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `topic` | string | Yes | Onboarding topic: "coreValues", "purpose", or "vision" | +| `message` | string | Yes | User's question or request for help (min 1 char) | +| `context` | object | No | Business context dictionary (key-value pairs) | + +**Response Model:** `OnboardingCoachingResponse` + +**Response Payload Structure:** + +```json +{ + "response": "string", + "suggestions": ["string"] +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `response` | string | AI coach's response | +| `suggestions` | array of string (optional) | Suggested values/statements | + +--- + +#### Topic: `business_metrics` + +Retrieve business metrics and data for coaching context. + +**Response Model:** `BusinessMetricsResponse` + +**Response Payload Structure:** + +```json +{ + "tenant_id": "string", + "business_data": {} +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `tenant_id` | string | The tenant identifier | +| `business_data` | object | Dictionary containing various business metrics and data | + +--- + +#### Topic: `niche_review` + +Review and suggest variations for business niche. + +**Request Payload Structure:** + +```json +{ + "topic_id": "niche_review", + "parameters": { + "current_value": "We help small business owners with marketing" + } +} +``` + +**Request Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `current_value` | string | Yes | Current niche text to review and improve | + +**Response Model:** `OnboardingReviewResponse` + +**Response Payload Structure:** + +The `data` field contains: + +```json +{ + "qualityReview": "string", + "suggestions": [ + { + "text": "string", + "reasoning": "string" + } + ] +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `qualityReview` | string | AI review of the current content quality with feedback. Use newlines (\n) to separate sections like Overall Assessment, Strengths, Weaknesses, and Suggestions for readability | +| `suggestions` | array of SuggestionVariation | Exactly 3 suggested variations | + +**SuggestionVariation Structure:** + +| Field | Type | Description | +|-------|------|-------------| +| `text` | string | The suggested text variation | +| `reasoning` | string | Explanation of why this variation is recommended. Use newlines (\n) to separate paragraphs for readability | + +**Example Response:** + +```json +{ + "topic_id": "niche_review", + "success": true, + "data": { + "qualityReview": "Your niche is clear but could be more specific...", + "suggestions": [ + { + "text": "We help B2B SaaS startups under $5M ARR build predictable revenue pipelines", + "reasoning": "More specific target market (B2B SaaS, revenue stage) and clear outcome" + }, + { + "text": "We help local service businesses attract high-value clients through digital marketing", + "reasoning": "Specifies business type and value proposition" + }, + { + "text": "We help e-commerce brands scale past $1M revenue with data-driven marketing strategies", + "reasoning": "Clear niche, growth stage, and methodology" + } + ] + }, + "schema_ref": "OnboardingReviewResponse", + "metadata": { + "model": "gpt-4o-mini", + "tokens_used": 450, + "processing_time_ms": 2340, + "finish_reason": "stop" + } +} +``` + +--- + +#### Topic: `ica_review` + +Review and suggest detailed ICA (Ideal Client Avatar) personas with demographics, goals, pain points, and buying behavior. + +**Request:** + +```json +{ + "topic_id": "ica_review", + "parameters": { + "current_value": "Business owners who want to grow" // Optional - can generate suggestions without a draft + } +} +``` + +**Response Model:** `IcaReviewResponse` + +**Response Payload Structure:** + +```typescript +{ + "qualityReview": "string | null", // Review of current ICA if provided, null otherwise + "suggestions": [ + { + "title": "string", // Descriptive persona name + "demographics": "string", // Age, gender, location, income, education, occupation, family + "goalsAspirations": "string", // What they want to achieve + "painPoints": "string", // Problems and frustrations they face + "motivations": "string", // What drives their decisions + "commonObjectives": "string", // Milestones they're working toward + "whereToFind": "string", // Channels, communities, platforms + "buyingProcess": "string" // How they research and make decisions + } + // Exactly 3 suggestions + ] +} +``` + +**Key Features:** +- **Optional current_value**: Can generate suggestions without existing ICA +- **Comprehensive personas**: Each suggestion includes 8 detailed fields +- **Business alignment**: Suggestions consider niche, value proposition, and products +- **Actionable insights**: Includes where to find prospects and their buying behavior + +--- + +#### Topic: `value_proposition_review` + +Review and suggest detailed value proposition variations with comprehensive positioning strategy. + +**Request:** + +```json +{ + "topic_id": "value_proposition_review", + "parameters": { + "current_value": "We provide great marketing services" // Optional - can generate suggestions without a draft + } +} +``` + +**Request Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `current_value` | string | No | Current value proposition to review and improve. If not provided, AI will generate suggestions from business context | + +**Response Model:** `ValuePropositionReviewResponse` + +**Response Payload Structure:** + +The `data` field contains: + +```json +{ + "qualityReview": "string or null", + "insufficientInformation": false, + "suggestions": [ + { + "uspStatement": "string", + "keyDifferentiators": ["string"], + "customerOutcomes": ["string"], + "proofPoints": ["string"], + "brandPromise": "string", + "primaryCompetitor": "string or null", + "competitiveAdvantage": "string", + "marketPosition": "Market Leader|Challenger|Niche Player|Emerging" + } + ] +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `qualityReview` | string or null | AI review of the current value proposition quality with feedback. Null if no current_value was provided or if there's insufficient information. Use newlines (\n) to separate sections for readability | +| `insufficientInformation` | boolean | True if there's not enough business context to generate quality suggestions. Default: false | +| `suggestions` | array of ValuePropositionSuggestion | Exactly 3 detailed value proposition suggestions with positioning strategy | + +**ValuePropositionSuggestion Structure:** + +| Field | Type | Description | +|-------|------|-------------| +| `uspStatement` | string | Unique Selling Proposition statement - the core value promise (10-500 chars) | +| `keyDifferentiators` | array of strings | 2-5 key differentiators that set the business apart from competitors | +| `customerOutcomes` | array of strings | 2-5 specific outcomes or benefits customers can expect | +| `proofPoints` | array of strings | 2-7 short credibility markers (testimonials, metrics, achievements, certifications, years in business, notable clients) | +| `brandPromise` | string | The brand promise - what the business commits to delivering consistently (10-300 chars) | +| `primaryCompetitor` | string or null | Primary competitor or competitive segment (if known/applicable), or null | +| `competitiveAdvantage` | string | Key competitive advantage that drives market differentiation (10-400 chars) | +| `marketPosition` | string | Market position: "Market Leader", "Challenger", "Niche Player", or "Emerging" | + +**Example Response:** + +```json +{ + "topic_id": "value_proposition_review", + "success": true, + "data": { + "qualityReview": "Your current value proposition is too generic and doesn't communicate specific value...\n\nStrengths:\n- Clear service category\n\nWeaknesses:\n- Lacks differentiation\n- No specific outcomes mentioned\n- Doesn't target a specific audience\n\nSuggestions:\n- Specify your target market\n- Highlight unique methodology or approach\n- Include measurable outcomes", + "insufficientInformation": false, + "suggestions": [ + { + "uspStatement": "We help B2B SaaS companies scale from $1M to $10M ARR through data-driven growth marketing that delivers predictable pipeline", + "keyDifferentiators": [ + "Specialized focus on B2B SaaS growth stage", + "Data-driven methodology with daily performance dashboards", + "Proven playbook for scaling ARR 10x", + "Full-stack marketing team included" + ], + "customerOutcomes": [ + "Predictable monthly pipeline generation", + "40%+ reduction in customer acquisition cost", + "3x increase in qualified lead volume within 90 days", + "Clear ROI tracking on every marketing dollar" + ], + "proofPoints": [ + "Helped 15+ SaaS companies achieve 10x ARR growth", + "Average 180% ROI in first 6 months", + "Featured in SaaStr and SaaS Weekly", + "Certified HubSpot and Google Premier Partner", + "12 years B2B SaaS marketing experience" + ], + "brandPromise": "We guarantee measurable pipeline growth within 90 days or work for free until you do", + "primaryCompetitor": "Full-service digital agencies without SaaS specialization", + "competitiveAdvantage": "Specialized SaaS growth expertise with proven scaling playbook and performance guarantees", + "marketPosition": "Niche Player" + }, + { + "uspStatement": "Transform your marketing chaos into a revenue engine with our AI-powered marketing platform and expert support", + "keyDifferentiators": [ + "AI-powered automation and optimization", + "White-glove onboarding and strategy support", + "All-in-one platform eliminating tool sprawl", + "Real-time performance insights" + ], + "customerOutcomes": [ + "Save 20+ hours per week on marketing tasks", + "Increase conversion rates by 35%+", + "Unified view of entire marketing funnel", + "Professional marketing without hiring full team" + ], + "proofPoints": [ + "500+ businesses trust our platform", + "4.8/5 star rating with 200+ reviews", + "Named G2 Leader in Marketing Automation", + "$50M+ in tracked customer revenue", + "99.9% platform uptime guarantee" + ], + "brandPromise": "We deliver marketing technology that actually works, backed by humans who care about your success", + "primaryCompetitor": "HubSpot", + "competitiveAdvantage": "Combines enterprise-grade AI automation with personalized human support at SMB-friendly pricing", + "marketPosition": "Challenger" + }, + { + "uspStatement": "Get world-class marketing strategy and execution without the enterprise price tag - perfect for growing businesses ready to scale", + "keyDifferentiators": [ + "Fractional CMO + execution team model", + "Enterprise expertise at mid-market pricing", + "Flexible month-to-month engagements", + "Industry-specific strategy frameworks" + ], + "customerOutcomes": [ + "Strategic marketing direction from day one", + "Professional brand positioning and messaging", + "Consistent content and campaign execution", + "Marketing that scales with your growth" + ], + "proofPoints": [ + "Former Fortune 500 marketing executives", + "Managed $100M+ in marketing budgets", + "50+ client success stories across 12 industries", + "Average 2.5 year client relationship", + "Published authors and conference speakers" + ], + "brandPromise": "We bring Fortune 500 marketing expertise to growing businesses who deserve better than generic agencies", + "primaryCompetitor": "Traditional marketing agencies", + "competitiveAdvantage": "Senior-level strategic expertise combined with execution capabilities at accessible pricing for mid-market", + "marketPosition": "Niche Player" + } + ] + }, + "schema_ref": "ValuePropositionReviewResponse", + "metadata": { + "model": "gpt-4o-mini", + "tokens_used": 1250, + "processing_time_ms": 4500, + "finish_reason": "stop" + } +} +``` + +**Insufficient Information Example:** + +```json +{ + "topic_id": "value_proposition_review", + "success": true, + "data": { + "qualityReview": "Not enough information provided to generate quality value proposition suggestions. Please provide:\n- Target market or niche description\n- Products/services offered\n- Ideal customer profile\n- Any unique aspects of your business approach", + "insufficientInformation": true, + "suggestions": [ + { + "uspStatement": "Unable to generate specific USP without business context", + "keyDifferentiators": ["More information needed", "Business context required"], + "customerOutcomes": ["Specific outcomes depend on your business model", "Customer benefits require market understanding"], + "proofPoints": ["Proof points require business history", "Credentials need business context"], + "brandPromise": "Brand promise requires understanding of your business values and approach", + "primaryCompetitor": null, + "competitiveAdvantage": "Competitive advantage analysis requires market and offering details", + "marketPosition": "Emerging" + }, + { + "uspStatement": "Comprehensive value proposition requires detailed business information", + "keyDifferentiators": ["Business differentiation needs context", "Unique value requires market understanding"], + "customerOutcomes": ["Customer results depend on service offering", "Outcomes require target market knowledge"], + "proofPoints": ["Track record information needed", "Credibility markers require business details"], + "brandPromise": "Promise development needs business mission and values", + "primaryCompetitor": null, + "competitiveAdvantage": "Advantage statement requires competitive landscape understanding", + "marketPosition": "Emerging" + }, + { + "uspStatement": "Strategic positioning requires foundational business information", + "keyDifferentiators": ["Differentiators need business context", "Competitive factors require market data"], + "customerOutcomes": ["Value delivery depends on business model", "Benefits require offering details"], + "proofPoints": ["Success indicators need business history", "Evidence requires operational context"], + "brandPromise": "Promise crafting needs core business values", + "primaryCompetitor": null, + "competitiveAdvantage": "Strategic advantage analysis requires comprehensive business understanding", + "marketPosition": "Emerging" + } + ] + }, + "schema_ref": "ValuePropositionReviewResponse", + "metadata": { + "model": "gpt-4o-mini", + "tokens_used": 450, + "processing_time_ms": 1800, + "finish_reason": "stop" + } +} +``` + +--- + +### Insights Topics + +#### Topic: `insights_generation` + +Generate leadership insights using KISS framework (Keep, Improve, Start, Stop) based on current business state, measures, and purpose alignment. + +**Request Payload Structure:** + +```json +{ + "topic_id": "insights_generation", + "parameters": { + "page": 1, + "page_size": 20, + "category": "strategy", // Optional filter + "priority": "high", // Optional filter + "status": "active" // Optional filter + } +} +``` + +**Request Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `page` | number | No | Page number for pagination (default: 1) | +| `page_size` | number | No | Items per page (default: 20, max: 100) | +| `category` | string | No | Filter by category: strategy, operations, finance, marketing, leadership, technology | +| `priority` | string | No | Filter by priority: critical, high, medium, low | +| `status` | string | No | Filter by status: active, dismissed, acknowledged, in_progress, completed | + +**Auto-enriched Parameters:** `foundation` (vision, purpose, core values, target market), `goals` (with progress), `strategies` (linked to goals), `measures` (with current/target values), `recent_actions`, `open_issues` + +**Core Premise:** + +Purpose-driven businesses aligned with their values result in: +- Engaged employees who are motivated and productive +- Loyal customers who trust and advocate for the brand +- Improved bottom line through sustainable growth + +**KISS Framework:** + +Each insight is categorized using KISS: +- **KEEP**: What's working well and aligned with purpose/values (continue doing) +- **IMPROVE**: What's partially working but needs optimization +- **START**: What's missing that should be initiated +- **STOP**: What's misaligned or counterproductive (cease doing) + +**Response Model:** `InsightsGenerationResponse` + +**Response Payload Structure:** + +The LLM generates a list of insights (typically 5-10) in a single response. This is ephemeral data - the .NET backend handles persistence and adds system fields (id, status, timestamps). + +```typescript +{ + "insights": [ + { + "title": "string", + "description": "string", + "category": "strategy" | "operations" | "finance" | "marketing" | "leadership" | "technology", + "priority": "critical" | "high" | "medium" | "low", + "kiss_category": "keep" | "improve" | "start" | "stop", + "alignment_impact": "string", + "business_impact": "low" | "medium" | "high", + "effort_required": "low" | "medium" | "high" + } + // ... 4-9 more insights + ] +} +``` + +**Note:** System fields like `id`, `status`, `created_at`, `updated_at` are added by the .NET backend when persisting, not generated by the LLM. + +**Frontend Integration:** +1. Call `/ai/execute-async` with `topic_id: "insights_generation"` +2. Poll job status until complete +3. Receive insights array from Python AI service (ephemeral, no persistence) +4. **Persist to .NET Traction Service:** POST `/traction/api/v1/insights/batch` with insights array +5. .NET backend adds: `id`, `tenantId`, `status` (default "active"), `createdAt`, `updatedAt` +6. Subsequent page loads fetch from .NET backend via GET `/traction/api/v1/insights` (not Python AI) +7. **Widget displays insights:** GET `/traction/api/v1/dashboard/widgets/ai-insights/data` with camelCase properties + +**Related Specifications:** +- **Persistence & CRUD:** See [Coaching Insights API](../../user-app/traction-service/insights-api.md) for POST /batch, GET, PUT, DELETE endpoints +- **Widget Display:** See [Dashboard Service - AI Insights Widget](../../user-app/dashboard-service.md#ai-insights-widget) for widget data endpoint + +--- + +### Strategic Planning Topics + +#### Topic: `strategy_suggestions` + +Generate strategic planning suggestions. + +**Request:** + +```json +{ + "topic_id": "strategy_suggestions", + "parameters": { + "goal_id": "abc-123" + } +} +``` + +**Response Model:** `StrategySuggestionsResponse` + +**Response Structure:** + +| Field | Type | Description | +|-------|------|-------------| +| `suggestions` | StrategySuggestion[] | Array of strategy suggestions | +| `description` | string | What's happening, why it matters, with specific data references (20-2000 chars) | +| `category` | string | Business domain: "strategy", "operations", "finance", "marketing", "leadership", "technology" | +| `priority` | string | Urgency level: "critical", "high", "medium", "low" | +| `kiss_category` | string | KISS framework: "keep", "improve", "start", "stop" | +| `alignment_impact` | string | How this affects purpose/values alignment and business outcomes (max 500 chars) | +| `status` | string | Current status: "active", "dismissed", "acknowledged", "in_progress", "completed" | +| `created_at` | ISO8601 datetime | When insight was generated | +| `updated_at` | ISO8601 datetime | Last update timestamp | +| `metadata` | InsightMetadata | Additional insight metadata | + +**InsightMetadata Fields:** + +| Field | Type | Description | +|-------|------|-------------| +| `conversation_count` | number | Number of conversations contributing to this insight | +| `business_impact` | string | Business impact level: "low", "medium", "high" | +| `effort_required` | string | Implementation effort: "low", "medium", "high" | + +**PaginationMeta Fields:** + +| Field | Type | Description | +|-------|------|-------------| +| `page` | number | Current page number | +| `limit` | number | Items per page | +| `total` | number | Total number of insights (all pages) | +| `total_pages` | number | Total number of pages | + +**Analysis Focus:** + +Insights prioritize: +- Alignment gaps between vision/purpose/values and current execution +- Goals with low progress or behind schedule +- Strategies missing for critical goals +- Measures showing concerning trends (current far from target) +- Actions not aligned with priority goals +- Issues blocking strategic progress +- Patterns suggesting systemic misalignment + +**Example Response:** + +```json +{ + "topic_id": "insights_generation", + "success": true, + "data": { + "success": true, + "data": [ + { + "id": "insight-abc123", + "title": "Customer retention goal 30% behind target - missing engagement strategy", + "description": "Based on measure 'Customer Retention Rate' (current: 70%, target: 90%), your customer retention goal is significantly behind schedule. Analysis shows no strategies defined for customer engagement or retention, despite this being aligned with your core value 'Customer First'. This gap is impacting both customer loyalty and bottom line growth.", + "category": "strategy", + "priority": "high", + "kiss_category": "start", + "alignment_impact": "Starting a customer engagement strategy aligns with your core value 'Customer First' and directly supports your purpose of 'empowering businesses to grow sustainably'. Improved retention drives loyal customers and recurring revenue.", + "status": "active", + "created_at": "2026-02-02T10:00:00Z", + "updated_at": "2026-02-02T10:00:00Z", + "metadata": { + "conversation_count": 0, + "business_impact": "high", + "effort_required": "medium" + } + }, + { + "id": "insight-def456", + "title": "Operational efficiency improving - maintain focus on process optimization", + "description": "Based on measure 'Task Completion Rate' (current: 88%, target: 90%), your operations team is performing well and making steady progress. The current approach to process documentation and automation aligns with your value of 'Excellence'. Continue investing in these areas to sustain momentum.", + "category": "operations", + "priority": "medium", + "kiss_category": "keep", + "alignment_impact": "Maintaining this operational excellence supports your purpose and demonstrates commitment to doing things right. This builds team confidence and customer trust.", + "status": "active", + "created_at": "2026-02-02T10:00:00Z", + "updated_at": "2026-02-02T10:00:00Z", + "metadata": { + "conversation_count": 0, + "business_impact": "medium", + "effort_required": "low" + } + } + ], + "pagination": { + "page": 1, + "limit": 20, + "total": 8, + "total_pages": 1 + } + }, + "schema_ref": "PaginatedInsightResponse", + "metadata": { + "model": "anthropic.claude-3-5-sonnet-20241022-v2:0", + "tokens_used": 3245, + "processing_time_ms": 4567, + "finish_reason": "stop" + } +} +``` + +**Frontend Implementation Notes:** + +```typescript +// Example TypeScript usage +const response = await coachingClient.post('/ai/execute', { + topic_id: 'insights_generation', + parameters: { + page: 1, + page_size: 20, + category: 'strategy', // Optional filter + priority: 'high' // Optional filter + } +}); + +// Access the insights array +const insights = response.data.data.data; // response.data.data.data[] for nested structure +const pagination = response.data.data.pagination; + +// Or access directly if using the legacy /insights/generate endpoint +const directResponse = await coachingClient.post('/insights/generate', { + page: 1, + page_size: 20, + category: 'strategy' +}); +const directInsights = directResponse.data.data; // Direct access +const directPagination = directResponse.data.pagination; + +// Process insights +insights.forEach(insight => { + console.log(`[${insight.kiss_category.toUpperCase()}] ${insight.title}`); + console.log(`Priority: ${insight.priority}, Impact: ${insight.metadata.business_impact}`); + console.log(`Alignment: ${insight.alignment_impact}`); +}); +``` + +**Notes:** + +- The AI analyzes current business state using measure data (current vs target values) to assess performance +- Suggests KISS actions (Keep, Improve, Start, Stop) that maintain purpose-driven alignment +- Each insight includes specific data references (goals, measures, strategies) in the description +- `alignment_impact` explains how the insight affects values alignment, employee engagement, and bottom line +- **IMPORTANT:** This endpoint generates NEW insights using LLM (costs money!) - cache results on frontend/backend + +--- + +### Strategic Planning Topics + +#### Topic: `goal_intent_review` + +Review and suggest goal intent statements that define WHAT to achieve and WHY, ensuring clarity and business alignment. + +**Purpose:** +Help users craft effective goal intents that focus on desired outcomes (WHAT) and business rationale (WHY), not actions or strategies (HOW). Validates that intents are not too action-focused and align with business foundation. + +**Request Payload Structure:** + +```json +{ + "topic_id": "goal_intent_review", + "parameters": { + "current_intent": "Implement a customer success program", + "goalId": "goal-123" + } +} +``` + +**Request Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `current_intent` | string | No | Draft goal intent to review (optional) | +| `goalId` | string | No | ID of the goal (optional, for context) | + +**Auto-enriched Parameters:** +- `vision` - Business vision statement +- `purpose` - Business purpose statement +- `core_values` - List of core values +- `target_market` - Target market description +- `value_proposition` - Value proposition +- `goal_title` - Goal title (if goalId provided) +- `goal_description` - Goal description (if goalId provided) +- `current_strategies` - Existing strategies (if goalId provided) +- `current_measures` - Existing measures (if goalId provided) +- `other_goals` - All other goals for context + +**Key Concepts:** +- **INTENT**: Defines WHAT we want to achieve and WHY (business outcome). Example: "Increase customer retention to build long-term relationships and sustainable revenue growth" +- **STRATEGY**: Defines HOW we will achieve the intent. Example: "Implement customer success program" +- **MEASURE**: Defines WHEN and HOW MUCH. Example: "Customer Retention Rate reaches 90% by Q4" + +**Functionality:** +1. **Quality Review** (if current intent provided): + - Validates the intent is not a strategy or action + - Scores quality from 0-100 + - Identifies if it lacks WHY component or is too action-focused +2. **Intent Suggestions**: Generates exactly 3 intent variations that: + - Focus on desired outcomes (WHAT + WHY) + - Align with business vision, purpose, and core values + - Are specific enough to guide strategy but not prescriptive + - Are realistic yet ambitious + +**Response Model:** `GoalIntentReviewResponse` + +**Response Payload Structure:** + +```json +{ + "qualityReview": "string or null", + "qualityScore": 75, + "suggestions": [ + { + "title": "Customer Retention Focus", + "intentStatement": "Increase customer retention to build long-term relationships and create predictable recurring revenue", + "explanation": "This intent clearly defines the desired outcome (increased retention) and the business rationale (long-term relationships and revenue stability)", + "strengthens": ["clarity", "alignment", "outcome-focus", "measurability"], + "alignmentHighlights": { + "vision": "Aligns with vision of becoming the trusted partner for growing businesses", + "purpose": "Supports purpose of empowering sustainable business growth", + "values": [ + "Customer First: Prioritizes long-term customer relationships", + "Sustainability: Focuses on predictable, recurring revenue" + ] + } + } + ] +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `qualityReview` | string \| null | Assessment of current intent (if provided), otherwise null | +| `qualityScore` | number \| null | Quality score 0-100 (if current intent provided), otherwise null | +| `suggestions` | array of IntentSuggestion | List of 3 intent statement variations | + +**IntentSuggestion Structure:** + +| Field | Type | Description | +|-------|------|-------------| +| `title` | string | Descriptive label for this intent variation (5-100 chars) | +| `intentStatement` | string | The suggested goal intent statement (20-300 chars) | +| `explanation` | string | Why this intent is effective and aligned (50-500 chars) | +| `strengthens` | array of string | List of 2-4 aspects this intent strengthens (clarity, alignment, motivation, etc.) | +| `alignmentHighlights` | object | How this intent connects to business foundation | + +**AlignmentHighlights Structure:** + +| Field | Type | Description | +|-------|------|-------------| +| `vision` | string | How intent connects to business vision | +| `purpose` | string | How intent serves business purpose | +| `values` | array of string | How intent aligns with specific core values | + +**Quality Score Interpretation:** +- **90-100**: Excellent intent - clear, aligned, outcome-focused +- **70-89**: Good intent - minor improvements needed +- **50-69**: Moderate intent - significant refinement required +- **30-49**: Weak intent - may be too action-focused or misaligned +- **0-29**: Poor intent - needs complete rework, likely a strategy or action + +**Usage Notes:** +- Intent should describe desired END STATE, not actions to take +- Avoid prescriptive language like "implement", "create", "launch" - these are strategies +- Focus on business outcomes and value creation +- The WHY component explains business rationale and expected impact + +--- + +#### Topic: `strategy_suggestions` + +Generate strategy suggestions for a specific goal, including review of existing strategies for alignment and efficiency. + +**Request Payload Structure:** + +```json +{ + "topic_id": "strategy_suggestions", + "parameters": { + "goalId": "goal-123", + "goalIntent": "Increase customer retention by 20%", + "businessContext": { + "targetMarket": "Small to medium businesses", + "valueProposition": "Comprehensive solutions with personal service", + "businessName": "Sample Business", + "industry": "Software", + "businessType": "B2B SaaS", + "currentChallenges": ["High churn", "Competition"] + }, + "constraints": { + "budget": 50000, + "timeline": "6 months", + "resources": ["2 developers", "1 designer"] + } + } +} +``` + +**Request Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `goalId` | string | Yes | The unique identifier of the goal requiring strategies | +| `goalIntent` | string | No | Optional goal intent/description (5-500 chars). If not provided, will be extracted from goal data | +| `businessContext` | BusinessContext | No | Optional additional business context. Business foundation (vision, purpose, core values) is auto-enriched | +| `constraints` | Constraints | No | Resource constraints for strategy implementation | + +**BusinessContext Structure (optional):** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `targetMarket` | string | No | Target market description | +| `valueProposition` | string | No | Value proposition statement | +| `businessName` | string | No | Business name | +| `industry` | string | No | Industry sector | +| `businessType` | string | No | Type of business (e.g., "B2B SaaS", "E-commerce") | +| `currentChallenges` | array of string | No | List of current business challenges | + +**Constraints Structure (optional):** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `budget` | number | No | Budget constraint in dollars | +| `timeline` | string | No | Timeline constraint (e.g., "6 months", "Q2 2025") | +| `resources` | array of string | No | Available resources (e.g., ["2 developers", "1 designer"]) | + +**Auto-enriched Parameters:** +- `goal` - Complete goal data (from `goal_id`) +- `goal_title` - Goal title +- `goal_description` - Goal description +- `business_foundation` - Vision, purpose, core values +- `strategies` - All strategies (filtered by `goal_id` in template to get strategies for this goal) + +**Functionality:** +1. **Strategy Review**: If the goal has existing strategies, the AI will review each one for: + - Alignment with the goal and business foundation + - Efficiency and effectiveness + - Areas for improvement or optimization +2. **New Strategy Suggestions**: Generates 3-5 new strategy suggestions that: + - Directly support the specific goal + - Align with business foundation (vision, purpose, core values) + - Consider existing strategies to avoid duplication + - Provide diverse strategic options + +**Response Model:** `StrategySuggestionsResponse` + +**Response Payload Structure:** + +```json +{ + "suggestions": [ + { + "title": "string", + "description": "string", + "rationale": "string", + "difficulty": "low" | "medium" | "high", + "timeframe": "string", + "expectedImpact": "low" | "medium" | "high", + "prerequisites": ["string"], + "estimatedCost": number | null, + "requiredResources": ["string"] + } + ], + "confidence": number, + "reasoning": "string" +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `suggestions` | array of StrategySuggestion | List of strategy suggestions | +| `confidence` | number (0-1) | Confidence score for suggestions | +| `reasoning` | string | Overall reasoning for suggestions | + +**StrategySuggestion Structure:** + +| Field | Type | Description | +|-------|------|-------------| +| `title` | string | Strategy title | +| `description` | string | Detailed strategy description | +| `rationale` | string | Why this strategy makes sense | +| `difficulty` | string | Implementation difficulty: "low", "medium", or "high" | +| `timeframe` | string | Expected timeframe (e.g., "2-3 months") | +| `expectedImpact` | string | Expected impact level: "low", "medium", or "high" | +| `prerequisites` | array of string | Prerequisites for implementation | +| `estimatedCost` | number \| null | Estimated cost in dollars (optional) | +| `requiredResources` | array of string | Required resources | + +--- + +#### Topic: `measure_recommendations` + +Recommend catalog measures for a goal or strategy, with suggested owner assignment based on positions/roles. + +**Request Payload Structure:** + +```json +{ + "topic_id": "measure_recommendations", + "parameters": { + "goal_id": "goal-123", + "strategy_id": "strategy-456" + } +} +``` + +**Request Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `goal_id` | string | Yes | ID of the goal to recommend measures for | +| `strategy_id` | string | No | Optional: ID of specific strategy to recommend measures for. If provided, measures will be associated with the strategy rather than the goal directly | + +**Auto-enriched Parameters:** +- `goal` - Goal data from goal_id +- `strategies` - All strategies (filtered by goal_id in template) +- `business_context` - Business foundation (vision, purpose, core values) +- `existing_measures` - Existing measures for the tenant +- `measure_catalog` - Complete measure catalog data +- `catalog_measures` - List of available catalog measures from measure library +- `tenant_custom_measures` - List of tenant custom measures +- `roles` - List of all roles in the organization +- `positions` - List of all positions in the organization +- `people` - List of all people in the organization + +**Response Model:** `MeasureRecommendationsResponse` (aliases to `KPIRecommendationsResponseV2`) + +**Response Payload Structure:** + +The `data` field contains: + +```json +{ + "recommendations": [ + { + "name": "string", + "description": "string", + "unit": "string", + "direction": "up" | "down", + "type": "quantitative" | "qualitative" | "binary", + "reasoning": "string", + "suggestedTarget": { + "value": "string", + "timeframe": "string", + "rationale": "string" + } | null, + "measurementApproach": "string", + "measurementFrequency": "daily" | "weekly" | "monthly" | "quarterly", + "isPrimaryCandidate": boolean, + "catalogMeasureId": "string | null", + "suggestedOwnerId": "string | null", + "suggestedOwnerName": "string | null", + "suggestedPositionId": "string | null", + "associationType": "goal" | "strategy" | null, + "associatedEntityId": "string | null" + } + ], + "analysisNotes": "string" +} +``` + +**Key Features:** +- **Catalog Measures**: AI recommends measures from the measure catalog when available, falling back to custom measures only if no suitable catalog measure exists +- **Owner Assignment**: AI suggests appropriate person/position to assign as measure owner based on role accountability, position responsibilities, and measure category +- **Strategy Support**: Measures can be associated with either a goal (default) or a specific strategy (if strategy_id provided) + +**Response Fields:** + +| Field | Type | Description | +|-------|------|-------------| +| `recommendations` | array of MeasureRecommendation | List of measure recommendations (1-5 items) | +| `analysisNotes` | string | Overall analysis and reasoning (50-300 chars) | + +**MeasureRecommendation Structure:** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | string (5-50 chars) | Measure name | +| `description` | string (20-300 chars) | What it measures and why it matters | +| `unit` | string (1-20 chars) | Unit of measurement (e.g., '%', 'USD', 'count') | +| `direction` | string | Desired direction: "up" or "down" | +| `type` | string | Measure type: "quantitative", "qualitative", or "binary" | +| `reasoning` | string (50-300 chars) | Why this measure is recommended | +| `suggestedTarget` | SuggestedTarget \| null | Optional suggested target | +| `measurementApproach` | string (20-200 chars) | How to measure this measure | +| `measurementFrequency` | string | Measurement frequency: "daily", "weekly", "monthly", or "quarterly" | +| `isPrimaryCandidate` | boolean | Whether this should be the primary measure | +| `catalogMeasureId` | string \| null | ID of recommended catalog measure (if from catalog) | +| `suggestedOwnerId` | string \| null | Suggested person ID to assign as measure owner | +| `suggestedOwnerName` | string \| null | Suggested person name to assign as measure owner | +| `suggestedPositionId` | string \| null | Suggested position ID (optional, if position-based assignment) | +| `associationType` | string \| null | Whether measure is for "goal" or "strategy" | +| `associatedEntityId` | string \| null | Goal ID or Strategy ID this measure is associated with | + +**SuggestedTarget Structure (optional):** + +| Field | Type | Description | +|-------|------|-------------| +| `value` | number | Target value | +| `timeframe` | string | Timeframe for achieving target (e.g., 'Q4 2025') | +| `rationale` | string | Rationale for this target | + +**Example Response:** + +```json +{ + "topic_id": "measure_recommendations", + "success": true, + "data": { + "recommendations": [ + { + "name": "Monthly Recurring Revenue", + "description": "Total predictable revenue from subscriptions, critical for tracking revenue growth", + "unit": "USD", + "direction": "up", + "type": "quantitative", + "reasoning": "Directly measures revenue growth goal and aligns with business model", + "suggestedTarget": { + "value": "150000", + "timeframe": "Q1 2025", + "rationale": "Based on current growth trajectory and market conditions" + }, + "measurementApproach": "Sum of all active subscription values at month end", + "measurementFrequency": "monthly", + "isPrimaryCandidate": true, + "catalogMeasureId": "catalog-001", + "suggestedOwnerId": "person-456", + "suggestedOwnerName": "Jane Doe", + "suggestedPositionId": "position-789", + "associationType": "goal", + "associatedEntityId": "goal-123" + } + ], + "analysisNotes": "Recommended catalog measures that align with your goal and business foundation. Suggested owners are based on role accountability and measure category." + }, + "schema_ref": "MeasureRecommendationsResponse" +} +``` + +--- + +#### Topic: `alignment_check` + +Calculate alignment score between a goal and business foundation, considering both the goal itself and the strategies chosen to implement it. + +**Request Payload Structure:** + +```json +{ + "topic_id": "alignment_check", + "parameters": { + "goal_id": "goal-123" + } +} +``` + +**Request Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `goal_id` | string | Yes | ID of goal to check alignment for | + +**Auto-enriched Parameters:** `goal`, `business_foundation` (vision, purpose, core_values), `strategies` (implementation strategies for the goal) + +**Response Model:** `AlignmentCheckResponse` + +**Response Payload Structure:** + +The `data` field contains: + +```json +{ + "alignmentScore": number, + "explanation": "string", + "suggestions": ["string"], + "breakdown": { + "visionAlignment": number, + "purposeAlignment": number, + "valuesAlignment": number + } +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `alignmentScore` | number (0-100) | Overall alignment score | +| `explanation` | string (50-500 chars) | Human-readable explanation of the alignment | +| `suggestions` | array of string (0-3 items) | Actionable improvement suggestions | +| `breakdown` | AlignmentBreakdown | Breakdown of alignment scores by component | + +**AlignmentBreakdown Structure:** + +| Field | Type | Description | +|-------|------|-------------| +| `visionAlignment` | number (0-100) | Alignment with company vision | +| `purposeAlignment` | number (0-100) | Alignment with company purpose | +| `valuesAlignment` | number (0-100) | Alignment with core values | + +**Example Response:** + +```json +{ + "topic_id": "alignment_check", + "success": true, + "data": { + "alignmentScore": 85, + "explanation": "Your goal shows strong alignment with your stated purpose and values. The goal directly supports your vision of 'Lead with clarity. Grow with purpose.' However, it could be strengthened by adding specific metrics tied to your core value of Integrity.", + "suggestions": [ + "Add specific metrics to measure success against your core values", + "Define how this goal will demonstrate 'Do it Right' in execution", + "Include milestones that reflect your commitment to Joy and Freedom" + ], + "breakdown": { + "visionAlignment": 90, + "purposeAlignment": 88, + "valuesAlignment": 78 + } + }, + "schema_ref": "AlignmentCheckResponse", + "metadata": { + "model": "gpt-5.2-2025-12-11", + "tokens_used": 3124, + "processing_time_ms": 6290, + "finish_reason": "stop" + } +} +``` + +--- + +#### Topic: `alignment_explanation` + +Get detailed explanation of an alignment score. + +**Request Payload Structure:** + +```json +{ + "topic_id": "alignment_explanation", + "parameters": { + "goal_id": "goal-123", + "alignment_score": 85 + } +} +``` + +**Request Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `goal_id` | string | Yes | ID of goal | +| `alignment_score` | integer | Yes | Previously calculated alignment score (0-100) | + +**Response Model:** `AlignmentExplanationResponse` + +**Response Payload Structure:** + +```json +{ + "explanation": "string", + "key_factors": ["string"], + "improvement_areas": ["string"] +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `explanation` | string | Detailed explanation of alignment | +| `key_factors` | array of string | Key factors influencing the score | +| `improvement_areas` | array of string | Areas for improvement | + +--- + +#### Topic: `alignment_suggestions` + +Get suggestions to improve goal alignment with business foundation. + +**Request Payload Structure:** + +```json +{ + "topic_id": "alignment_suggestions", + "parameters": { + "goal_id": "goal-123", + "alignment_score": 65 + } +} +``` + +**Request Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `goal_id` | string | Yes | ID of goal | +| `alignment_score` | integer | Yes | Previously calculated alignment score (0-100) | + +**Response Model:** `AlignmentSuggestionsResponse` + +**Response Payload Structure:** + +```json +{ + "suggestions": ["string"], + "impact_analysis": "string" +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `suggestions` | array of string | List of suggestions to improve alignment | +| `impact_analysis` | string | Analysis of potential impact | + +**Example Response:** + +```json +{ + "topic_id": "alignment_suggestions", + "success": true, + "data": { + "suggestions": [ + "Reframe the goal to emphasize customer impact", + "Add specific metrics that align with core values", + "Connect the goal more explicitly to your vision statement" + ], + "impact_analysis": "These changes could improve alignment by 15-20 points..." + { + "suggestion": "Add specific metrics tied to core values", + "expected_improvement": 10, + "reasoning": "Makes integrity and excellence measurable" + } + ], + "potential_score": 90 + }, + "schema_ref": "AlignmentSuggestionsResponse" +} +``` + +--- + +### Operations AI Topics + +#### Topic: `root_cause_suggestions` + +Get AI suggestions for root causes of an issue. + +**Request:** + +```json +{ + "topic_id": "root_cause_suggestions", + "parameters": { + "issue_id": "issue-456" + } +} +``` + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `issue_id` | string | Yes | ID of issue to analyze | + +**Auto-enriched Parameters:** `issue`, `business_context` + +**Response Model:** `RootCauseSuggestionsResponse` + +```json +{ + "topic_id": "root_cause_suggestions", + "success": true, + "data": { + "issue_summary": "Declining customer retention rate", + "potential_causes": [ + { + "cause": "Inadequate onboarding process", + "likelihood": "high", + "evidence": "Customer feedback mentions confusion in first 30 days", + "investigation_steps": ["Review onboarding completion rates", "Survey churned customers"] + }, + { + "cause": "Feature gaps vs. competitors", + "likelihood": "medium", + "evidence": "Exit surveys mention missing integrations", + "investigation_steps": ["Competitive feature analysis", "Review feature requests"] + } + ] + }, + "schema_ref": "RootCauseSuggestionsResponse" +} +``` + +--- + +#### Topic: `swot_analysis` + +Generate a SWOT analysis based on business context. + +**Request:** + +```json +{ + "topic_id": "swot_analysis", + "parameters": {} +} +``` + +**Auto-enriched Parameters:** `business_foundation`, `goals`, `strategies`, `measures_summary` + +**Response Model:** `SwotAnalysisResponse` + +**Status:** Inactive + +--- + +#### Topic: `five_whys_questions` + +Generate Five Whys analysis questions. + +**Response Model:** `FiveWhysQuestionsResponse` + +**Status:** Inactive + +--- + +#### Topic: `action_suggestions` + +Get AI-suggested actions for a goal or specific strategy. + +**Request Payload Structure:** + +```json +{ + "topic_id": "action_suggestions", + "parameters": { + "goal_id": "goal-123", + "strategy_id": "strategy-456" // Optional + } +} +``` + +**Request Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `goal_id` | string | Yes | ID of the goal to generate action suggestions for | +| `strategy_id` | string | No | Optional: ID of specific strategy to generate actions for. If omitted, generates actions for all strategies under the goal | + +**Auto-enriched Parameters:** `goal`, `strategies`, `business_foundation` + +**Behavior:** +- **Without `strategy_id`**: Generates actions for all strategies under the goal (covers entire goal execution) +- **With `strategy_id`**: Generates actions only for that specific strategy (focused recommendations) + +**Response Model:** `ActionSuggestionsResponse` + +**Response Payload Structure:** + +The `data` field contains: + +```json +{ + "suggestions": [ + { + "title": "string", + "description": "string", + "reasoning": "string", + "priority": "low" | "medium" | "high" | "critical", + "estimatedDuration": "string", + "suggestedOwnerRole": "string" | null, + "dependencies": ["string"], + "sequenceOrder": number + } + ], + "analysisNotes": "string", + "timelineEstimate": "string" | null +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `suggestions` | array of ActionSuggestion | List of action suggestions (1-10 items) | +| `analysisNotes` | string (50-200 chars) | Meta-commentary on the suggestions | +| `timelineEstimate` | string \| null | Overall timeline estimate (optional) | + +**ActionSuggestion Structure:** + +| Field | Type | Description | +|-------|------|-------------| +| `title` | string (5-100 chars) | Action title | +| `description` | string (50-500 chars) | Detailed action description | +| `reasoning` | string (50-200 chars) | Why this action is important | +| `priority` | string | Priority level: "low", "medium", "high", or "critical" | +| `estimatedDuration` | string | Human-readable duration estimate (e.g., '2 weeks') | +| `suggestedOwnerRole` | string \| null | Suggested role for ownership (optional) | +| `dependencies` | array of string | Titles of prerequisite actions (0-3 items) | +| `sequenceOrder` | number (≥1) | Suggested execution order | +| `associatedStrategyId` | string \| null | Strategy ID this action supports (null for goal-level actions) | +| `associatedStrategyName` | string \| null | Strategy name this action supports | + +--- + +#### Topic: `optimize_action_plan` + +Optimize an existing action plan for better outcomes. + +**Request Payload Structure:** + +```json +{ + "topic_id": "optimize_action_plan", + "parameters": { + "goal_id": "goal-123" + } +} +``` + +**Request Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `goal_id` | string | Yes | ID of the goal whose action plan should be optimized | + +**Auto-enriched Parameters:** `goal`, `actions`, `business_context` + +**Response Model:** `OptimizedActionPlanResponse` + +**Response Payload Structure:** + +```json +{ + "optimized_plan": [ + { + "title": "string", + "description": "string", + "priority": "low" | "medium" | "high" | "critical", + "estimatedDuration": number, + "estimatedCost": number | null, + "assignmentSuggestion": "string" | null, + "dependencies": ["string"], + "confidence": number, + "reasoning": "string", + "expectedOutcome": "string, + "risks": ["string"] + } + ], + "optimization_rationale": "string", + "efficiency_gain": "string" +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `optimized_plan` | array of ActionSuggestion | Optimized list of actions | +| `optimization_rationale` | string | Why this plan is optimized | +| `efficiency_gain` | string | Estimated efficiency gain | + +**Note:** The `ActionSuggestion` structure in `optimize_action_plan` includes additional fields like `estimatedCost`, `confidence`, `expectedOutcome`, and `risks` compared to `action_suggestions`. + +--- + +#### Topic: `prioritization_suggestions` + +Suggest prioritization of operational tasks. + +**Request Payload Structure:** + +```json +{ + "topic_id": "prioritization_suggestions", + "parameters": { + "actions": [ + { + "id": "action-123", + "title": "Implement new feature", + "currentPriority": "medium", + "dueDate": "2025-03-15", + "impact": "High customer satisfaction", + "effort": "2 weeks", + "status": "in_progress", + "linkedGoals": ["goal-123"] + } + ], + "businessContext": { + "currentGoals": ["Increase revenue", "Improve retention"], + "constraints": ["Limited budget", "Tight timeline"], + "urgentDeadlines": ["Q1 launch"] + } + } +} +``` + +**Request Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `actions` | array of PrioritizationActionInput | Yes | Actions to prioritize (1-200 items) | +| `businessContext` | BusinessContext | Yes | Business context for prioritization | + +**PrioritizationActionInput Structure:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `id` | string | Yes | Unique action identifier | +| `title` | string | Yes | Action title (min 1 char) | +| `currentPriority` | string | Yes | Current priority: "low", "medium", "high", or "critical" | +| `dueDate` | string | No | Due date in ISO8601 format | +| `impact` | string | No | Expected impact description | +| `effort` | string | No | Required effort description | +| `status` | string | Yes | Current status | +| `linkedGoals` | array of string | No | Linked goal IDs | + +**BusinessContext Structure:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `currentGoals` | array of string | No | Current business goals | +| `constraints` | array of string | No | Business constraints | +| `urgentDeadlines` | array of string | No | Urgent deadlines | + +**Response Model:** `PrioritizationSuggestionsResponse` (maps to `PrioritizationResponse`) + +**Response Payload Structure:** + +The `data` field contains: + +```json +{ + "success": boolean, + "data": [ + { + "actionId": "string", + "suggestedPriority": "low" | "medium" | "high" | "critical", + "currentPriority": "low" | "medium" | "high" | "critical", + "reasoning": "string", + "confidence": number, + "urgencyFactors": ["string"], + "impactFactors": ["string"], + "recommendedAction": "escalate" | "maintain" | "de-prioritize", + "estimatedBusinessValue": number | null + } + ] +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `success` | boolean | Request success status | +| `data` | array of PrioritizationSuggestion | Prioritization suggestions | + +**PrioritizationSuggestion Structure:** + +| Field | Type | Description | +|-------|------|-------------| +| `actionId` | string | Action identifier | +| `suggestedPriority` | string | Suggested priority level | +| `currentPriority` | string | Current priority level | +| `reasoning` | string | Reasoning for suggestion | +| `confidence` | number (0-1) | Confidence score | +| `urgencyFactors` | array of string | Factors contributing to urgency | +| `impactFactors` | array of string | Factors contributing to impact | +| `recommendedAction` | string | Recommended action | +| `estimatedBusinessValue` | number \| null | Estimated business value in currency units (optional) | + +--- + +#### Topic: `scheduling_suggestions` + +Suggest optimal scheduling for tasks. + +**Request Payload Structure:** + +```json +{ + "topic_id": "scheduling_suggestions", + "parameters": { + "actions": [ + { + "id": "action-123", + "title": "Implement new feature", + "estimatedDuration": 80, + "dependencies": ["action-456"], + "assignedTo": "user-789", + "currentStartDate": "2025-02-01", + "currentDueDate": "2025-02-15", + "priority": "high" + } + ], + "constraints": { + "teamCapacity": 160, + "criticalDeadlines": [ + { + "date": "2025-03-01", + "description": "Q1 launch deadline" + } + ], + "teamAvailability": [ + { + "personId": "user-789", + "hoursPerWeek": 40, + "unavailableDates": ["2025-02-10"] + } + ] + } + } +} +``` + +**Request Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `actions` | array of SchedulingActionInput | Yes | Actions to schedule (1-100 items) | +| `constraints` | SchedulingConstraints | Yes | Scheduling constraints | + +**SchedulingActionInput Structure:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `id` | string | Yes | Unique action identifier | +| `title` | string | Yes | Action title (min 1 char) | +| `estimatedDuration` | number | Yes | Estimated duration in hours (must be > 0) | +| `dependencies` | array of string | No | Action IDs this depends on | +| `assignedTo` | string | No | Assigned person/team ID | +| `currentStartDate` | string | No | Current start date in ISO8601 format | +| `currentDueDate` | string | No | Current due date in ISO8601 format | +| `priority` | string | Yes | Priority level: "low", "medium", "high", or "critical" | + +**SchedulingConstraints Structure:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `teamCapacity` | number | Yes | Total team capacity in hours (must be > 0) | +| `criticalDeadlines` | array of CriticalDeadline | No | Critical deadline constraints | +| `teamAvailability` | array of TeamAvailability | No | Team member availability | + +**CriticalDeadline Structure:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `date` | string | Yes | Deadline date in ISO8601 format | +| `description` | string | Yes | Deadline description | + +**TeamAvailability Structure:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `personId` | string | Yes | Person/team member identifier | +| `hoursPerWeek` | number | Yes | Available hours per week (1-168) | +| `unavailableDates` | array of string | No | Unavailable dates in ISO8601 format | + +**Response Model:** `SchedulingSuggestionsResponse` (maps to `SchedulingResponse`) + +**Response Payload Structure:** + +The `data` field contains: + +```json +{ + "success": boolean, + "data": [ + { + "actionId": "string", + "suggestedStartDate": "string (ISO8601)", + "suggestedDueDate": "string (ISO8601)", + "reasoning": "string", + "confidence": number, + "dependencies": ["string"], + "resourceConsiderations": ["string"], + "risks": ["string"], + "alternativeSchedules": [ + { + "startDate": "string (ISO8601)", + "dueDate": "string (ISO8601)", + "rationale": "string" + } + ] + } + ] +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `success` | boolean | Request success status | +| `data` | array of SchedulingSuggestion | Scheduling suggestions | + +**SchedulingSuggestion Structure:** + +| Field | Type | Description | +|-------|------|-------------| +| `actionId` | string | Action identifier | +| `suggestedStartDate` | string | Suggested start date (ISO8601) | +| `suggestedDueDate` | string | Suggested due date (ISO8601) | +| `reasoning` | string | Reasoning for schedule | +| `confidence` | number (0-1) | Confidence score | +| `dependencies` | array of string | Dependencies that influenced schedule | +| `resourceConsiderations` | array of string | Resource considerations | +| `risks` | array of string | Identified risks | +| `alternativeSchedules` | array of AlternativeSchedule | Alternative scheduling options (optional) | + +**AlternativeSchedule Structure:** + +| Field | Type | Description | +|-------|------|-------------| +| `startDate` | string | Alternative start date (ISO8601) | +| `dueDate` | string | Alternative due date (ISO8601) | +| `rationale` | string | Rationale for this alternative | + +--- + +#### Topic: `categorize_issue` + +Categorize operational issue by type and severity. + +**Response Model:** `IssueCategoryResponse` + +**Status:** Inactive + +--- + +#### Topic: `assess_impact` + +Assess business impact of operational issue. + +**Response Model:** `ImpactAssessmentResponse` + +**Status:** Inactive + +--- + +### Operations-Strategic Integration Topics + +*Note: Most topics in this category are currently inactive. Only active topics are listed below.* + +#### Topic: `action_strategic_context` + +Get strategic context for a specific action. + +**Response Model:** `ActionStrategicContextResponse` + +**Status:** Inactive + +--- + +#### Topic: `suggest_connections` + +Suggest strategic connections for actions. + +**Response Model:** `SuggestedConnectionsResponse` + +**Status:** Inactive + +--- + +#### Topic: `update_connections` + +Update strategic connections for an action. + +**Response Model:** `UpdateConnectionsResponse` + +**Status:** Inactive + +--- + +#### Topic: `create_issue_from_action` + +Create an issue from an incomplete action. + +**Response Model:** `CreateIssueResponse` + +**Status:** Inactive + +--- + +#### Topic: `create_action_from_issue` + +Create action items from an issue. + +**Response Model:** `CreateActionResponse` + +**Status:** Inactive + +--- + +#### Topic: `complete_action` + +Complete an action and update related items. + +**Response Model:** `CompleteActionResponse` + +**Status:** Inactive + +--- + +#### Topic: `close_issue` + +Close an issue and update related items. + +**Response Model:** `CloseIssueResponse` + +**Status:** Inactive + +--- + +#### Topic: `issue_status` + +Get comprehensive status of an issue. + +**Response Model:** `IssueStatusResponse` + +**Status:** Inactive + +--- + +#### Topic: `issue_related_actions` + +Get actions related to an issue. + +**Response Model:** `RelatedActionsResponse` + +**Status:** Inactive + +--- + +#### Topic: `update_measure` + +Update a measure value with audit trail. + +**Response Model:** `UpdateMeasureResponse` + +**Status:** Inactive + +--- + +#### Topic: `calculate_measure` + +Calculate measure value from linked data. + +**Response Model:** `CalculateMeasureResponse` + +**Status:** Inactive + +--- + +#### Topic: `measure_history` + +Get historical values for a measure. + +**Response Model:** `MeasureHistoryResponse` + +**Status:** Inactive + +--- + +#### Topic: `measure_impact` + +Analyze measure impact on strategic goals. + +**Response Model:** `MeasureImpactResponse` + +**Status:** Inactive + +--- + +#### Topic: `action_measure_impact` + +Calculate impact of action completion on measures. + +**Response Model:** `ActionMeasureImpactResponse` + +**Status:** Inactive + +--- + +#### Topic: `sync_measures_to_strategy` + +Sync operational measures to strategic planning. + +**Response Model:** `SyncMeasuresResponse` + +**Status:** Inactive + +--- + +#### Topic: `detect_measure_conflicts` + +Detect conflicts between operational and strategic measures. + +**Response Model:** `MeasureConflictsResponse` + +**Status:** Inactive + +--- + +#### Topic: `resolve_measure_conflict` + +Resolve a detected measure conflict. + +**Response Model:** `ResolveConflictResponse` + +**Status:** Inactive + +--- + +#### Topic: `operations_strategic_alignment` + +Get alignment status between operations and strategy. + +**Response Model:** `StrategicAlignmentResponse` + +**Status:** Inactive + +--- + +#### Topic: `cascade_action_update` + +Cascade action updates to related items. + +**Response Model:** `CascadeUpdateResponse` + +**Status:** Inactive + +--- + +#### Topic: `cascade_issue_update` + +Cascade issue updates to related items. + +**Response Model:** `CascadeUpdateResponse` + +**Status:** Inactive + +--- + +#### Topic: `cascade_measure_update` + +Cascade measure updates to related strategic items. + +**Response Model:** `CascadeUpdateResponse` + +**Status:** Inactive + +--- + +### Analysis Topics + +#### Topic: `alignment_analysis` + +Analyze goal alignment with business foundation. + +**Response Model:** `GoalAlignmentResponse` + +**Note:** Response model structure not yet defined in codebase. Schema will be available via `GET /ai/schemas/GoalAlignmentResponse` once implemented. + +--- + +#### Topic: `measure_analysis` + +Analyze measure performance and trends. + +**Request:** + +```json +{ + "topic_id": "measure_analysis", + "parameters": { + "performance_data": { + "period": "Q4 2025", + "metrics": [...] + } + } +} +``` + +**Auto-enriched Parameters:** `measures` + +**Response Model:** `MeasurePerformanceResponse` + +**Note:** Response model structure not yet defined in codebase. Schema will be available via `GET /ai/schemas/MeasurePerformanceResponse` once implemented. + +--- + +#### Topic: `operations_analysis` + +Perform operational analysis (SWOT, root cause, action plan). + +**Request Payload Structure:** + +```json +{ + "topic_id": "operations_analysis", + "parameters": { + "analysis_type": "swot", + "description": "We're experiencing declining customer retention and need to understand the root causes and develop an action plan.", + "context": { + "industry": "SaaS", + "companySize": "50-100 employees", + "recentChanges": ["New pricing model", "Product update"] + } + } +} +``` + +**Request Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `analysis_type` | string | Yes | Type of operational analysis: "swot", "root_cause", or "action_plan" | +| `description` | string | Yes | Description of situation/problem (10-10000 chars) | +| `context` | object | No | Additional operational context (key-value pairs) | + +**Response Model:** `OperationsAnalysisResponse` + +**Response Payload Structure:** + +```json +{ + "analysis_id": "string", + "analysis_type": "string", + "specific_analysis_type": "string", + "findings": {}, + "recommendations": [ + {} + ], + "priority_actions": ["string"], + "created_at": "string (ISO8601)", + "metadata": {} +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `analysis_id` | string | Analysis identifier | +| `analysis_type` | string | Analysis type | +| `specific_analysis_type` | string | Specific operational analysis type (e.g., "swot", "root_cause", "action_plan") | +| `findings` | object | Analysis findings (structure varies by type) | +| `recommendations` | array of object | Actionable recommendations | +| `priority_actions` | array of string | High-priority actions | +| `created_at` | string | Analysis timestamp (ISO8601) | +| `metadata` | object | Additional metadata | + +**Note:** The `findings` structure varies based on `specific_analysis_type`. For SWOT analysis, it may contain `strengths`, `weaknesses`, `opportunities`, and `threats` arrays. + +--- + +#### Topic: `topic_strategic_context` + +Get strategic context for admin topic management. + +**Response Model:** `TopicStrategicContextResponse` + +**Status:** Inactive + +--- + +## Conversation Topics + +### Topic: `core_values` + +Discover and articulate your organization's authentic core values through guided coaching. + +**Type:** Conversation Coaching +**Category:** Onboarding +**Result Model:** `CoreValuesResult` + +**Response Payload Structure (During Conversation):** + +During the conversation, responses follow the `ConversationResponse` structure: + +```json +{ + "message": "string", + "is_final": boolean, + "result": null | CoreValuesResult, + "confidence": number +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `message` | string | AI coach's conversational response | +| `is_final` | boolean | Whether the conversation has concluded | +| `result` | CoreValuesResult \| null | Final result when `is_final` is true | +| `confidence` | number (0-1) | Confidence score for the response | + +**Final Result Schema (when `is_final` is true):** + +```json +{ + "values": [ + { + "name": "string (1-100 chars)", + "description": "string (10-500 chars)", + "importance": "string (10-500 chars)" + } + ], + "summary": "string (50-1000 chars)" +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `values` | array of CoreValue | List of identified core values (1-7 items) | +| `summary` | string (50-1000 chars) | Summary of the core values | + +**CoreValue Structure:** + +| Field | Type | Description | +|-------|------|-------------| +| `name` | string (1-100 chars) | Value name (e.g., "Integrity", "Innovation") | +| `description` | string (10-500 chars) | What this value means to the organization | +| `importance` | string (10-500 chars) | Why this value matters and how it guides decisions | + +**Endpoints:** Use `/ai/coaching/start`, `/ai/coaching/message`, `/ai/coaching/complete` + +--- + +### Topic: `purpose` + +Define your organization's deeper purpose and reason for existing through guided coaching. + +**Type:** Conversation Coaching +**Category:** Onboarding +**Result Model:** `PurposeResult` + +**Response Payload Structure (During Conversation):** + +Same as `core_values` - responses follow the `ConversationResponse` structure with `message`, `is_final`, `result`, and `confidence` fields. + +**Final Result Schema (when `is_final` is true):** + +```json +{ + "purpose_statement": "string (20-500 chars)", + "why_it_matters": "string (50-1000 chars)", + "how_it_guides": "string (50-1000 chars)" +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `purpose_statement` | string (20-500 chars) | The organization's purpose statement | +| `why_it_matters` | string (50-1000 chars) | Why this purpose is meaningful | +| `how_it_guides` | string (50-1000 chars) | How purpose guides decisions | + +**Endpoints:** Use `/ai/coaching/start`, `/ai/coaching/message`, `/ai/coaching/complete` + +--- + +### Topic: `vision` + +Craft a compelling vision for your organization's future through guided coaching. + +**Type:** Conversation Coaching +**Category:** Onboarding +**Result Model:** `VisionResult` + +**Response Payload Structure (During Conversation):** + +Same as `core_values` - responses follow the `ConversationResponse` structure with `message`, `is_final`, `result`, and `confidence` fields. + +**Final Result Schema (when `is_final` is true):** + +```json +{ + "vision_statement": "string (20-500 chars)", + "time_horizon": "string (1-50 chars)", + "key_aspirations": ["string"] +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `vision_statement` | string (20-500 chars) | The organization's vision statement | +| `time_horizon` | string (1-50 chars) | Time frame for the vision (e.g., "5 years", "10 years") | +| `key_aspirations` | array of string (1-10 items) | Key aspirations that comprise the vision | + +**Endpoints:** Use `/ai/coaching/start`, `/ai/coaching/message`, `/ai/coaching/complete` + +--- + +## Response Model Schemas + +### OnboardingReviewResponse + +Used by: `niche_review`, `value_proposition_review` + +```typescript +interface OnboardingReviewResponse { + qualityReview: string; // AI review with feedback + suggestions: SuggestionVariation[]; // Exactly 3 suggestions +} + +interface SuggestionVariation { + text: string; // Suggested text variation + reasoning: string; // Why this variation is recommended +} +``` + +### IcaReviewResponse + +Used by: `ica_review` + +```typescript +interface IcaReviewResponse { + qualityReview: string | null; // Review of current ICA if provided, null if no current_value + suggestions: IcaSuggestion[]; // Exactly 3 detailed persona suggestions +} + +interface IcaSuggestion { + title: string; // Descriptive persona name (5-100 chars) + demographics: string; // Age, gender, location, income, education, occupation, family (20-500 chars) + goalsAspirations: string; // What they want to achieve, ambitions (20-500 chars) + painPoints: string; // Problems, challenges, frustrations (20-500 chars) + motivations: string; // What drives them, values, priorities (20-500 chars) + commonObjectives: string; // Typical goals and milestones (20-500 chars) + whereToFind: string; // Channels, communities, platforms (20-500 chars) + buyingProcess: string; // Research and decision-making process (20-500 chars) +} +``` + +### AlignmentAnalysisResponse + +Used by: `alignment_check` + +```typescript +interface AlignmentAnalysisResponse { + alignment_score: number; // 0-100 + alignment_level: "low" | "medium" | "high"; + factors: AlignmentFactor[]; +} + +interface AlignmentFactor { + factor: string; + score: number; + reasoning: string; +} +``` + +### MeasureRecommendationsResponse + +Used by: `measure_recommendations` + +```typescript +interface MeasureRecommendationsResponse { + recommendations: MeasureRecommendation[]; + total_recommendations: number; +} + +interface MeasureRecommendation { + name: string; + description: string; + unit: string; + direction: "up" | "down" | "maintain"; + frequency: string; + reasoning: string; +} +``` + +**JSON Schemas:** Available via `GET /ai/schemas/{schema_name}` + +--- + +## Error Handling + +All errors follow the standard API error format: + +```json +{ + "detail": "Error message here" +} +``` + +### Common Error Codes + +| Status | Error | Resolution | +|--------|-------|------------| +| 404 | `Topic not found: {topic_id}` | Topic not found in topic registry. Check `topic_id` against `/ai/topics` | +| 400 | `Topic is not active: {topic_id}` | Topic is disabled in registry or DynamoDB, contact admin | +| 400 | `Topic {topic_id} is type {type}` | Use appropriate endpoint based on TopicType (single-shot vs conversation) | +| 422 | `Missing required parameters: [...]` | Include all required parameters as defined in topic registry | +| 500 | `Response model not configured` | Response model not found in response model registry | +| 500 | `Parameter enrichment failed` | Backend data source unavailable | + +### Coaching Conversation Error Codes + +| Status | Code | Description | +|--------|------|-------------| +| 400 | SESSION_NOT_ACTIVE | Session is not in an active state | +| 400 | VALIDATION_ERROR | Request validation failed | +| 403 | SESSION_ACCESS_DENIED | User does not have access to this session | +| 409 | SESSION_CONFLICT | Another user has an active session | +| 410 | SESSION_EXPIRED | Session has expired | +| 410 | SESSION_IDLE_TIMEOUT | Session exceeded idle timeout | +| 422 | SESSION_NOT_FOUND | Session not found | +| 422 | MAX_TURNS_REACHED | Maximum conversation turns reached | +| 422 | INVALID_TOPIC | Topic not found or invalid | +| 500 | EXTRACTION_FAILED | Failed to extract results | + +--- + +## Usage Examples + +### JavaScript/TypeScript + +```typescript +// Basic execution with request parameters only +const alignmentResult = await coachingClient.post('/ai/execute', { + topic_id: 'alignment_check', + parameters: { + goal_id: 'goal-123' // Only need to provide IDs + } +}); + +// The backend automatically fetches goal details and business foundation +const { alignment_score, factors } = alignmentResult.data.data; + +// Onboarding review (requires full content) +const nicheReview = await coachingClient.post('/ai/execute', { + topic_id: 'niche_review', + parameters: { + current_value: 'We help small businesses grow' + } +}); + +const { qualityReview, suggestions } = nicheReview.data.data; +``` + +### Python + +```python +import httpx + +async def check_alignment(goal_id: str) -> dict: + async with httpx.AsyncClient() as client: + response = await client.post( + f"{BASE_URL}/ai/execute", + json={ + "topic_id": "alignment_check", + "parameters": {"goal_id": goal_id} + }, + headers={"Authorization": f"Bearer {token}"} + ) + return response.json()["data"] +``` + +### Async Execution + +```typescript +// 1. Start async job +const { jobId, status } = await coachingClient.post('/ai/execute-async', { + topic_id: 'niche_review', + parameters: { current_value: 'We help small businesses grow' } +}); + +// 2. Listen for WebSocket events +websocket.on('message', (event) => { + const data = JSON.parse(event.data); + if (data.type === 'ai.job.completed' && data.data.jobId === jobId) { + handleResult(data.data.result); + } +}); +``` + +### Coaching Conversation + +```typescript +// Start or resume a coaching session +async function startCoachingSession(topicId: string, context?: Record) { + const response = await coachingClient.post('/ai/coaching/start', { + topic_id: topicId, + context + }); + return response.data.data; +} + +// Send a message and get coach response +async function sendMessage(sessionId: string, message: string) { + const response = await coachingClient.post('/ai/coaching/message', { + session_id: sessionId, + message + }); + return response.data.data; +} +``` + +--- + +## Changelog + +### Version 2.1 (January 15, 2026) + +- **Major Restructure:** Added revision log at top, comprehensive table of contents with hyperlinks +- **Reorganization:** Moved async execution and coaching conversation endpoints to Core Endpoints section +- **Topic Organization:** Created Supported Topics summary with hyperlinks organized by category +- **Parameter Reference:** Moved available parameters section after index +- **Complete Topic List:** Verified and listed all topics from registry by category (single-shot first, then conversation) +- **Response Models:** Verified all response models match the codebase registry +- **Architectural Clarification:** Documented topic-centric architecture, clarified topic data sources (registry, DynamoDB, S3, seed data), and explained endpoint routing based on TopicType + +### Version 2.0 (January 2026) + +- **Terminology Update:** Replaced all "KPI" references with "Measure" +- **New Data Sources:** Added strategy, measure, people, and organization parameters +- **Parameter Enrichment:** Documented automatic parameter enrichment system +- **New Topics:** Added strategic planning topics (alignment_check, alignment_explanation, alignment_suggestions, strategy_suggestions, measure_recommendations) +- **New Topics:** Added operations AI topics (root_cause_suggestions, swot_analysis, action_suggestions, optimize_action_plan) +- **New Topics:** Added analysis topics (measure_analysis) +- **Parameter Reference:** Complete reference of all available parameters by source +- **Response Models:** Added schemas for new response models + +### Version 1.2 (December 2025) + +- Added async execution endpoints +- Added coaching conversation sessions +- Added website_scan topic + +--- + +_This document is maintained in sync with the topic registry in the codebase. All topics are defined in `coaching/src/core/topic_registry.py`._ diff --git a/docs/shared/Specifications/ai-user/coaching-session-workflow.md b/docs/shared/Specifications/ai-user/coaching-session-workflow.md new file mode 100644 index 00000000..1a32f18a --- /dev/null +++ b/docs/shared/Specifications/ai-user/coaching-session-workflow.md @@ -0,0 +1,95 @@ +# Coaching Session Workflow Guide + +**Version:** 3.0 +**Last Updated:** February 12, 2026 +**Status:** Active + +[← Back to Backend Integration](./backend-integration-unified-ai.md) + +--- + +## Purpose + +This guide explains the coaching journey in human terms: how a session starts, pauses, resumes, and completes from a user experience perspective. + +**Technical API and payload contracts live in one place only:** +- `docs/shared/Specifications/ai-user/backend-integration-unified-ai.md` +- `docs/shared/Specifications/eventbridge/async-coaching-message-events.md` + +--- + +## Session Lifecycle + +Coaching sessions move through these states: + +- **ACTIVE**: User can continue messaging +- **PAUSED**: User paused or frontend treats an idle session as paused on reopen +- **COMPLETED**: Conversation is done and results are available +- **CANCELLED**: User cancelled the session +- **ABANDONED**: Older session replaced by a fresh start + +The main lifecycle principle: +- **Start means new conversation** +- **Resume means continue existing conversation** + +--- + +## User Journey + +### 1) User opens coaching + +The frontend checks if there is an existing session and whether there is a conflict with another user in the tenant. + +- If no session exists, user starts fresh. +- If a resumable session exists, user chooses to resume or start new. +- If there is a conflict, the UI blocks entry and shows guidance. + +### 2) User sends a message + +Message handling is asynchronous: + +- The API immediately accepts the request and returns a job identifier. +- The backend processes the message in the background. +- Frontend receives the result by WebSocket, or polling fallback. + +This avoids request timeouts on long model responses and keeps the UI responsive. + +### 3) Conversation continues or ends + +Each completed message indicates whether the conversation is final: + +- If not final, the user keeps chatting. +- If final, the UI presents extracted results and closes or transitions the session. + +### 4) User pauses, resumes, or starts over + +- **Pause** explicitly blocks additional messages until resume. +- **Resume** re-enters the existing session context. +- **Start new** creates a fresh session and retires the old one. + +--- + +## Idle and Pause Behavior + +Idle time is treated as a UX signal, not a hard failure for active work: + +- A user who still has an active context can continue conversation flow. +- On re-entry, frontend can present the session as paused and offer resume/new options. + +--- + +## Operational Notes + +- Sessions have TTL-based cleanup. +- Async jobs are temporary and used for status tracking and delivery. +- Frontend should treat job completion events idempotently. + +--- + +## Quick Checklist + +- Frontend checks session status before opening coaching UI +- Frontend handles resume vs start-new explicitly +- Message send uses async job flow +- Completion handling uses finality flag + result payload +- Pause/resume actions are reflected in UI state diff --git a/docs/shared/Specifications/eventbridge/async-coaching-message-events.md b/docs/shared/Specifications/eventbridge/async-coaching-message-events.md new file mode 100644 index 00000000..02730561 --- /dev/null +++ b/docs/shared/Specifications/eventbridge/async-coaching-message-events.md @@ -0,0 +1,1218 @@ +# Async Coaching Message Events - Frontend Integration Specification + +**Version**: 1.3.0 +**Date**: February 10, 2026 +**Related Issue**: #222 +**Target Audience**: Frontend Developers, .NET Backend Team + +## Overview + +This specification defines the complete async pattern for coaching conversation messages, including HTTP APIs, WebSocket events, error handling, and polling fallback. Messages are processed asynchronously (5s - 5min) to avoid API Gateway 30s timeout. + +**Single Source of Truth**: This document contains all contracts needed for frontend implementation. + +## Field Naming Conventions + +### HTTP Responses (FastAPI - snake_case) + +All HTTP request/response bodies use **`snake_case`**: + +- `job_id`, `session_id`, `is_final`, `max_turns`, `message_count`, `error_code`, `processing_time_ms`, `estimated_duration_ms` + +**Example**: +```json +{ + "job_id": "uuid", + "is_final": false, + "max_turns": 10 +} +``` + +### WebSocket Payloads (EventBridge - camelCase) + +All WebSocket event payloads use **`camelCase`**: + +- `jobId`, `sessionId`, `isFinal`, `maxTurns`, `messageCount`, `errorCode`, `eventType` + +**Example**: +```json +{ + "jobId": "uuid", + "isFinal": false, + "maxTurns": 10 +} +``` + +### Summary + +| Transport | Casing | Fields | +|-----------|--------|--------| +| **HTTP (POST/GET)** | `snake_case` | `job_id`, `is_final`, `error_code` | +| **WebSocket Events** | `camelCase` | `jobId`, `isFinal`, `errorCode` | + +### Why Async Pattern? + +**Problem**: Coaching AI responses can take 20-90 seconds due to: +- Complex conversation context processing +- Prompt cache writes (20-30s first response) +- Final message extraction with structured output + +**Solution**: Return 202 Accepted immediately, process async, deliver via WebSocket + +### Message Flow + +``` +┌──────────┐ ┌──────────┐ +│ Frontend │ │ Backend │ +└────┬─────┘ └────┬─────┘ + │ │ + │ POST /ai/coaching/message │ + │ { session_id, message } │ + ├──────────────────────────────────────────────>│ + │ │ + │ 202 Accepted │ + │ { job_id, session_id, status: "pending" } │ + │<──────────────────────────────────────────────┤ + │ │ + │ [Show "AI is thinking..." UI] │ + │ │ + │ │ [Processing 5s-5min] + │ │ [LLM generates response] + │ │ + │ ┌──────────────┐ │ + │ WebSocket Event │ EventBridge │ │ + │ ai.message.completed │ ↓ │ │ + │<───────────────────────┤ WebSocket │<──────┤ + │ │ Service │ │ + │ { message, isFinal } └──────────────┘ │ + │ │ + │ [Display complete response] │ + │ [If isFinal: show results] │ + │ │ +``` + +## API Contracts + +### POST /ai/coaching/message + +**Endpoint**: +``` +POST /ai/coaching/message +Authorization: Bearer +Content-Type: application/json +``` + +**Request**: +```json +{ + "session_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "message": "I value integrity and transparency" +} +``` + +**Success Response - 202 Accepted** (ALWAYS): +```json +{ + "success": true, + "data": { + "job_id": "550e8400-e29b-41d4-a716-446655440000", + "session_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "status": "pending", + "estimated_duration_ms": 45000 + }, + "message": "Message job created, processing asynchronously" +} +``` + +**Important**: This endpoint **ALWAYS returns 202 Accepted**. It never returns 200 with the assistant message. + +**Error Responses**: + +| HTTP Status | Error Code | Scenario | Retryable | +|-------------|-----------|----------|-----------| +| 422 | `JOB_VALIDATION_ERROR` | Empty message | No | +| 422 | `SESSION_NOT_FOUND` | Session doesn't exist | No | +| 400 | `SESSION_NOT_ACTIVE` | Session paused/completed/cancelled | No | +| 403 | `SESSION_ACCESS_DENIED` | User doesn't own session | No | +| 422 | `MAX_TURNS_REACHED` | Hit conversation limit | No | +| 410 | `SESSION_IDLE_TIMEOUT` | Session expired from inactivity | No | +| 500 | `INTERNAL_ERROR` | Server error | Yes (after 30s) | + +**Example Error**: +```json +{ + "detail": { + "code": "SESSION_NOT_ACTIVE", + "message": "Session is not active (status: completed)" + } +} +``` + +#### One Message In-Flight Policy + +⚠️ **CRITICAL**: Backend does NOT enforce "one message per session in-flight". + +**Frontend MUST implement client-side**: +1. Track pending `job_id` per session (localStorage) +2. Disable send button while job pending +3. Only enable after `ai.message.completed` or `ai.message.failed` +4. On page reload: check localStorage for pending job → start polling + +**Future**: Backend may add `SESSION_BUSY` (409 Conflict) validation. + +### GET /ai/coaching/message/{job_id} + +**Endpoint** (polling fallback): +``` +GET /ai/coaching/message/{job_id} +Authorization: Bearer +``` + +**Use Case**: Fallback when WebSocket unavailable or for debugging. + +#### State: pending + +```json +{ + "success": true, + "data": { + "job_id": "550e8400-e29b-41d4-a716-446655440000", + "session_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "status": "pending", + "message": null, + "is_final": null, + "turn": null, + "max_turns": null, + "message_count": null, + "result": null, + "error": null, + "processing_time_ms": null + }, + "message": "Job status: pending" +} +``` + +#### State: processing + +```json +{ + "success": true, + "data": { + "job_id": "550e8400-e29b-41d4-a716-446655440000", + "session_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "status": "processing", + "message": null, + "is_final": null, + "turn": null, + "max_turns": null, + "message_count": null, + "result": null, + "error": null, + "processing_time_ms": null + }, + "message": "Job status: processing" +} +``` + +#### State: completed (non-final turn) + +```json +{ + "success": true, + "data": { + "job_id": "550e8400-e29b-41d4-a716-446655440000", + "session_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "status": "completed", + "message": "That's wonderful! Integrity shows up in how you communicate transparently with your team...", + "is_final": false, + "turn": 3, + "max_turns": 10, + "message_count": 6, + "result": null, + "error": null, + "processing_time_ms": 12450 + }, + "message": "Job status: completed" +} +``` + +✅ **Note**: `turn`, `max_turns`, and `message_count` are included in polling responses for parity with WebSocket completion semantics. + +#### State: completed (final turn with extraction) + +```json +{ + "success": true, + "data": { + "job_id": "550e8400-e29b-41d4-a716-446655440000", + "session_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "status": "completed", + "message": "Thank you for this wonderful conversation! Here are your three core values...", + "is_final": true, + "turn": 10, + "max_turns": 10, + "message_count": 20, + "result": { + "identified_values": [ + "Integrity: Transparent communication", + "Growth: Continuous learning", + "Innovation: Creative problem-solving" + ], + "extraction_type": "core_values", + "confidence_score": 0.95, + "metadata": { + "model_used": "claude-3-5-haiku", + "extraction_success": true + } + }, + "error": null, + "processing_time_ms": 18720 + }, + "message": "Job status: completed" +} +``` + +#### State: failed + +```json +{ + "success": true, + "data": { + "job_id": "550e8400-e29b-41d4-a716-446655440000", + "session_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "status": "failed", + "message": null, + "is_final": null, + "turn": null, + "max_turns": null, + "message_count": null, + "result": null, + "error": "Session not found: f47ac10b-58cc-4372-a567-0e02b2c3d479", + "processing_time_ms": 2100 + }, + "message": "Job status: failed" +} +``` + +#### Job Not Found (after 24hr TTL) + +**Status**: `404 Not Found` + +```json +{ + "detail": { + "code": "JOB_NOT_FOUND", + "message": "Message job not found: {job_id}" + } +} +``` + +--- + +## Completion & Extraction Semantics + +### When is_final = true with result = null? + +**Possible**: `is_final` can be `true` with `result` being `null` or containing error fields. + +#### Scenario 1: Extraction Parsing Failed + +Event still sends as `ai.message.completed` (NOT `ai.message.failed`): + +```json +{ + "eventType": "ai.message.completed", + "jobId": "uuid", + "data": { + "message": "Thank you for sharing your values...", + "isFinal": true, + "result": { + "parse_error": "Expecting value: line 1 column 1 (char 0)", + "raw_response": "Here are the values:\n- Integrity\n- Growth\n- Innovation" + } + } +} +``` + +**Frontend Action**: +- Check if `result.parse_error` or `result.validation_error` exists +- Show completion UI with warning: "We had trouble extracting your responses. Please review manually." + +#### Scenario 2: Extraction Model Not Configured + +```json +{ + "isFinal": true, + "result": {} // Empty object +} +``` + +**Frontend Action**: Show completion message without extraction results. + +### When does ai.message.failed occur? + +Only for **execution failures before message generation**: + +- `SESSION_NOT_FOUND` +- `SESSION_ACCESS_DENIED` +- `SESSION_NOT_ACTIVE` +- `SESSION_IDLE_TIMEOUT` +- `MAX_TURNS_REACHED` +- `LLM_TIMEOUT` +- `LLM_ERROR` +- `INTERNAL_ERROR` + +**NOT for extraction failures** - those still return `ai.message.completed` with error fields in `result`. + +## WebSocket Events + +### Event: `ai.message.completed` + +### Event: `ai.message.completed` + +**When Received**: After AI successfully generates a response (5s - 5min after POST) + +**Payload**: +```json +{ + "eventType": "ai.message.completed", + "jobId": "uuid", + "sessionId": "uuid", + "tenantId": "uuid", + "userId": "uuid", + "data": { + "message": "Complete AI coach response. This is the full response with no token streaming.", + "isFinal": false, + "turn": 3, + "maxTurns": 10, + "messageCount": 6, + "result": null + } +} +``` + +**Fields**: + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `eventType` | string | Yes | Always `"ai.message.completed"` | +| `jobId` | string (UUID) | Yes | Job identifier from 202 response | +| `sessionId` | string (UUID) | Yes | Coaching session identifier | +| `tenantId` | string (UUID) | Yes | Tenant identifier (for routing) | +| `userId` | string (UUID) | Yes | User identifier (for routing) | +| `data.message` | string | Yes | **Complete AI response** (full text, no streaming) | +| `data.isFinal` | boolean | Yes | `true` if conversation is ending | +| `data.turn` | number | Yes | Current turn number (1-indexed, coach responses only) | +| `data.maxTurns` | number | Yes | Maximum turns allowed (0 = unlimited) | +| `data.messageCount` | number | Yes | Total messages in conversation (user + AI) | +| `data.result` | object \| null | Yes | Extraction result when `isFinal` is `true` | + +**When `isFinal` is `true`** (conversation complete): + +```json +{ + "eventType": "ai.message.completed", + "jobId": "uuid", + "sessionId": "uuid", + "tenantId": "uuid", + "userId": "uuid", + "data": { + "message": "Final coach message summarizing the conversation...", + "isFinal": true, + "turn": 10, + "maxTurns": 10, + "messageCount": 20, + "result": { + "identified_values": ["value1", "value2", "value3"], + "extraction_type": "conversation_result", + "progress": 1.0, + "metadata": { + "model_used": "claude-3-5-haiku", + "extraction_success": true + } + } + } +} +``` + +**Result Object Fields**: + +| Field | Type | Description | +|-------|------|-------------| +| `identified_values` | string[] | Key values/insights identified in conversation | +| `extraction_type` | string | Always `"conversation_result"` | +| `progress` | number | Always `1.0` when final | +| `metadata` | object | Extraction metadata (model used, success flag) | + +### Event: `ai.message.failed` + +**When Received**: When message processing fails + +**Payload**: +```json +{ + "eventType": "ai.message.failed", + "jobId": "uuid", + "sessionId": "uuid", + "tenantId": "uuid", + "userId": "uuid", + "data": { + "error": "Session not found", + "errorCode": "SESSION_NOT_FOUND" + } +} +``` + +**Fields**: + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `eventType` | string | Yes | Always `"ai.message.failed"` | +| `jobId` | string (UUID) | Yes | Job identifier from 202 response | +| `sessionId` | string (UUID) | Yes | Coaching session identifier | +| `tenantId` | string (UUID) | Yes | Tenant identifier (for routing) | +| `userId` | string (UUID) | Yes | User identifier (for routing) | +| `data.error` | string | Yes | Human-readable error message | +| `data.errorCode` | string | Yes | Machine-readable error code | + +## Error Codes + +Complete error catalog with user messaging and retryability: + +| Error Code | User Message | Retryable | Action | +|-----------|--------------|-----------|--------| +| `SESSION_NOT_FOUND` | "Session not found. Please start a new conversation." | No | Clear session, show start button | +| `SESSION_ACCESS_DENIED` | "You don't have access to this session." | No | Redirect to home | +| `SESSION_NOT_ACTIVE` | "This session is no longer active." | No | Show resume or restart options | +| `SESSION_IDLE_TIMEOUT` | "Session expired due to inactivity." | No | Show restart button | +| `MAX_TURNS_REACHED` | "Conversation limit reached." | No | Show completion message | +| `JOB_VALIDATION_ERROR` | "Invalid request. Please try again." | No | Log error, show generic message | +| `PARAMETER_VALIDATION` | "Invalid parameters. Please try again." | No | Log error, show generic message | +| `LLM_TIMEOUT` | "AI response took too long. Please try again." | Yes | Show retry button (immediate) | +| `LLM_ERROR` | "AI service error. Please try again." | Yes | Show retry button (wait 10s) | +| `INTERNAL_ERROR` | "Something went wrong. Please try again." | Yes | Show retry button (wait 30s) | + +**Note**: `EXTRACTION_FAILED` does NOT appear in `ai.message.failed` events. Extraction failures return `ai.message.completed` with error fields in `result`. + +### Retry Strategy + +```typescript +const getRetryDelay = (errorCode: string): number => { + switch (errorCode) { + case 'LLM_TIMEOUT': + return 0; // Retry immediately + case 'LLM_ERROR': + return 10000; // Wait 10 seconds + case 'INTERNAL_ERROR': + return 30000; // Wait 30 seconds + default: + return -1; // Not retryable + } +}; + +const handleError = (event: MessageFailedEvent, retryAttempt: number) => { + const delay = getRetryDelay(event.data.errorCode); + + if (delay < 0 || retryAttempt >= 3) { + // Show non-retryable error or max retries reached + showError(getUserMessage(event.data.errorCode)); + return; + } + + // Show retryable error with countdown + showRetryableError(getUserMessage(event.data.errorCode), delay / 1000); + + setTimeout(() => { + // Retry sending the message + retryMessage(sessionId, originalMessage, retryAttempt + 1); + }, delay); +}; +``` + +## Frontend Implementation + +### React/TypeScript Example + +```typescript +interface CoachingMessageAPI { + sendMessage(sessionId: string, message: string): Promise; +} + +interface MessageJobResponse { + job_id: string; + session_id: string; + status: 'pending'; + estimated_duration_ms: number; +} + +interface MessageCompletedEvent { + eventType: 'ai.message.completed'; + jobId: string; + sessionId: string; + tenantId: string; + userId: string; + data: { + message: string; + isFinal: boolean; + turn: number; + maxTurns: number; + messageCount: number; + result: ConversationResult | null; + }; +} + +interface MessageFailedEvent { + eventType: 'ai.message.failed'; + jobId: string; + sessionId: string; + tenantId: string; + userId: string; + data: { + error: string; + errorCode: string; + }; +} + +// Sending a message +const handleSendMessage = async (message: string) => { + try { + // POST returns immediately with job_id + const response = await coachingAPI.sendMessage(sessionId, message); + + // Show "AI is thinking..." UI + setProcessingJobId(response.job_id); + setIsProcessing(true); + + // Wait for WebSocket event (handled in useEffect) + } catch (error) { + showError('Failed to send message'); + } +}; + +// WebSocket event handler +useEffect(() => { + if (!websocket) return; + + const handleMessage = (event: MessageEvent) => { + const payload = JSON.parse(event.data); + + switch (payload.eventType) { + case 'ai.message.completed': + handleMessageCompleted(payload as MessageCompletedEvent); + break; + + case 'ai.message.failed': + handleMessageFailed(payload as MessageFailedEvent); + break; + } + }; + + websocket.addEventListener('message', handleMessage); + return () => websocket.removeEventListener('message', handleMessage); +}, [websocket]); + +const handleMessageCompleted = (event: MessageCompletedEvent) => { + // Only process if job ID matches current processing + if (event.jobId !== processingJobId) return; + + setIsProcessing(false); + + // Add AI message to chat + addMessage({ + role: 'assistant', + content: event.data.message + }); + + // Update progress indicators + if (event.data.maxTurns > 0) { + setProgress(event.data.turn / event.data.maxTurns); + setProgressText(`Question ${event.data.turn} of ${event.data.maxTurns}`); + } + + // Handle conversation completion + if (event.data.isFinal) { + setConversationStatus('completed'); + setConversationResult(event.data.result); + showCompletionDialog(event.data.result); + } +}; + +const handleMessageFailed = (event: MessageFailedEvent) => { + if (event.jobId !== processingJobId) return; + + setIsProcessing(false); + + // Show user-friendly error message + const userMessage = getErrorMessage(event.data.errorCode); + showError(userMessage, event.data.errorCode); + + // Handle specific error types + if (event.data.errorCode === 'SESSION_NOT_FOUND') { + clearSession(); + } +}; + +const getErrorMessage = (code: string): string => { + const messages = { + 'SESSION_NOT_FOUND': 'Session not found. Please start a new conversation.', + 'SESSION_ACCESS_DENIED': "You don't have access to this session.", + 'LLM_TIMEOUT': 'AI response took too long. Please try again.', + // ... other codes + }; + return messages[code] || 'Something went wrong. Please try again.'; +}; +``` + +### UI State Management + +```typescript +// Processing state +const [isProcessing, setIsProcessing] = useState(false); +const [processingJobId, setProcessingJobId] = useState(null); + +// When sending message + + +// Show processing indicator +{isProcessing && ( +
+ + AI is thinking... + This may take up to 90 seconds +
+)} +``` + +### Polling Fallback (No WebSocket) + +```typescript +// For systems without WebSocket support +const pollMessageStatus = async (jobId: string) => { + const maxAttempts = 60; // 5 minutes with 5s intervals + let attempts = 0; + + while (attempts < maxAttempts) { + try { + const response = await fetch(`/ai/coaching/message/${jobId}`); + const data = await response.json(); + + if (data.data.status === 'completed') { + handleMessageCompleted({ + eventType: 'ai.message.completed', + jobId: jobId, + sessionId: data.data.session_id, + data: { + message: data.data.message, + isFinal: data.data.is_final, + turn: data.data.turn, + maxTurns: data.data.max_turns, + messageCount: data.data.message_count, + result: data.data.result + } + }); + return; + } + + if (data.data.status === 'failed') { + throw new Error(data.data.error); + } + + // Still processing, wait 5s + await new Promise(resolve => setTimeout(resolve, 5000)); + attempts++; + + } catch (error) { + showError('Failed to check message status'); + return; + } + } + + // Timeout after 5 minutes + showError('Message processing timed out'); +}; +``` + +## Event Type Field + +**All WebSocket events use the `eventType` field** to identify the event type. + +**Event types**: + +| Pattern | Event Type | Description | +|---------|-----------|-------------| +| **Async Messages** (Conversations) | `ai.message.completed` | Coach response ready | +| **Async Messages** (Conversations) | `ai.message.failed` | Message processing failed | +| **Single-shot Jobs** (Analysis) | `ai.job.completed` | Analysis job completed | +| **Single-shot Jobs** (Analysis) | `ai.job.failed` | Analysis job failed | + +**WebSocket handler example**: + +```typescript +const handleWebSocketMessage = (payload: any) => { + switch (payload.eventType) { + case 'ai.message.completed': + case 'ai.message.failed': + handleCoachingMessage(payload); + break; + + case 'ai.job.completed': + case 'ai.job.failed': + handleJobCompletion(payload); + break; + + default: + console.warn('Unknown event type:', payload.eventType); + } +}; +``` + +## Testing + +### Manual Testing (Frontend Dev) + +1. **Send Message**: + ```bash + curl -X POST https://api.dev.purposepath.ai/ai/coaching/message \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"session_id":"your-session-id","message":"Tell me about values"}' + ``` + +2. **Check Response**: + - Verify `202 Accepted` status + - Save `job_id` from response + +3. **Monitor WebSocket**: + - Open browser DevTools → Network → WS + - Wait for `ai.message.completed` event + - Verify `jobId` matches saved value + +4. **Test Error Handling**: + ```bash + # Invalid session ID + curl -X POST ... -d '{"session_id":"invalid","message":"test"}' + ``` + - Verify `ai.message.failed` event received + - Check `errorCode` is `"SESSION_NOT_FOUND"` + +### Integration Checklist + +- [ ] POST /message returns 202 with `job_id` +- [ ] "Processing" UI shows after POST +- [ ] WebSocket receives `ai.message.completed` event +- [ ] Complete message displays correctly +- [ ] `isFinal` flag triggers completion flow +- [ ] Error events show user-friendly messages +- [ ] Session cleared on `SESSION_NOT_FOUND` +- [ ] Polling fallback works without WebSocket + +## .NET Backend Notes + +The .NET WebSocket service must forward EventBridge events to frontend clients: + +**Subscribe to EventBridge events**: +- `ai.message.completed` (detail-type) +- `ai.message.failed` (detail-type) +- Filter by `detail.stage` matching environment + +**Forward to WebSocket clients**: +- Route by `detail.tenantId` + `detail.userId` +- Send complete EventBridge `detail` object as WebSocket message +- Frontend expects this exact structure + +**Do NOT forward**: +- `ai.message.created` events (internal worker trigger only) + +## Migration from Synchronous Pattern + +### Before (Synchronous) + +```typescript +// POST returned complete response after 20-90s +const response = await fetch('/ai/coaching/message', { + method: 'POST', + body: JSON.stringify({ session_id, message }) +}); + +// HTTP 200 with complete message +const data = await response.json(); +displayMessage(data.message); // Full response immediately +``` + +**Problems**: +- API Gateway 30s timeout → 503 errors for long responses +- User waited with blocking UI +- No progress indication + +### After (Asynchronous) + +```typescript +// POST returns immediately +const response = await fetch('/ai/coaching/message', { + method: 'POST', + body: JSON.stringify({ session_id, message }) +}); + +// HTTP 202 with job_id +const data = await response.json(); +showProcessing(data.job_id); + +// Wait for WebSocket event (5s - 5min later) +websocket.on('message', (event) => { + if (event.eventType === 'ai.message.completed') { + hideProcessing(); + displayMessage(event.data.message); + } +}); +``` + +**Benefits**: +- No API Gateway timeout (202 returns instantly) +- Non-blocking UI with progress indicator +- Lambda can process for up to 5 minutes +- Better error handling + +## Sequence Diagrams + +### Normal Flow + +``` +User Frontend API Gateway Lambda EventBridge WebSocket + | | | | | | + |-- Type msg --->| | | | | + | |-- POST ------>| | | | + | | |-- Invoke --->| | | + | | | |-- Publish ----->| | + | | | | (ai.message.created) | + | |<-- 202 -------|<-- Return ---| | | + |<-- "Thinking"--| | | | | + | | | | | | + | | | [Worker processes | | + | | | 5s - 5 minutes] | | + | | | | | | + | | | |-- Publish ----->| | + | | | | (ai.message.completed) | + | | | | |-- Forward -->| + | |<-- WebSocket event (complete message) <--------| | + |<-- Display ----| | | | | +``` + +### Error Flow + +``` +User Frontend API Gateway Lambda EventBridge WebSocket + | | | | | | + |-- Type msg --->| | | | | + | |-- POST ------>| | | | + | | |-- Invoke --->| | | + | | | |-- Publish ----->| | + | |<-- 202 -------|<-- Return ---| | | + |<-- "Thinking"--| | | | | + | | | | | | + | | | [Worker encounters | | + | | | error - LLM timeout] | | + | | | | | | + | | | |-- Publish ----->| | + | | | | (ai.message.failed) | + | | | | |-- Forward -->| + | |<-- WebSocket event (error) <------------------| | + |<-- Error UI ---| | | | | +``` + +--- + +## Implementation Guidelines + +### Event Ordering & Deduplication + +**Ordering Guarantees**: +- Events for the **same `jobId`** are delivered in order +- Events across different jobs have no ordering guarantee +- Sequence: `ai.message.created` (internal) → `ai.message.completed` OR `ai.message.failed` (never both) + +**Deduplication**: +- EventBridge may deliver duplicate events (rare, but possible) +- **Frontend must handle idempotently**: Check if `jobId` already processed before updating UI +- **One terminal event per job**: Either `completed` OR `failed`, never both + +**Routing**: +- Route by `jobId` ONLY (not sessionId) +- Multiple pending jobs per session are possible (since backend doesn't enforce one-in-flight) + +```typescript +const processedJobs = new Set(); + +const handleWebSocketEvent = (event: MessageCompletedEvent | MessageFailedEvent) => { + // Deduplication check + if (processedJobs.has(event.jobId)) { + console.warn('Duplicate event for job:', event.jobId); + return; + } + + processedJobs.add(event.jobId); + + // Process event... +}; +``` + +### Retention & TTL + +**Job Retention**: 24 hours from creation + +After 24 hours: +- `GET /message/{job_id}` returns `404 JOB_NOT_FOUND` +- Jobs automatically deleted from DynamoDB +- Frontend should not store `job_id` beyond 24 hours + +**Session Retention**: No automatic expiration + +- Paused sessions never expire automatically +- Idle timeout only marks session inactive (doesn't delete) +- Sessions persist until explicitly completed or cancelled + +**Recommendation**: Clear `job_id` from localStorage after receiving terminal event. + +### Polling Strategy + +**When to use**: +- Primary: WebSocket event delivery +- Fallback: Polling when WebSocket disconnected or safety timeout exceeded + +**Recommended polling**: + +```typescript +const pollMessageStatus = async (jobId: string) => { + const POLL_INTERVAL = 5000; // 5 seconds + const MAX_DURATION = 300000; // 5 minutes + const startTime = Date.now(); + + while (Date.now() - startTime < MAX_DURATION) { + try { + const response = await fetch(`/ai/coaching/message/${jobId}`); + const data = await response.json(); + + if (data.data.status === 'completed') { + handleCompleted(data.data); + return; + } + + if (data.data.status === 'failed') { + handleFailed(data.data); + return; + } + + // Still processing, wait before next poll + await sleep(POLL_INTERVAL); + + } catch (error) { + console.error('Polling error:', error); + await sleep(POLL_INTERVAL); + } + } + + // Timeout after 5 minutes + showError('Request timed out. Please try again.'); +}; +``` + +**Safety timeout for WebSocket**: + +```typescript +// Start polling if no WebSocket event received after 90s +const safetyTimeout = setTimeout(() => { + if (jobStillPending) { + console.warn('No WebSocket event after 90s, starting polling...'); + startPolling(jobId); + } +}, 90000); + +// Cancel timeout when WebSocket event arrives +websocket.addEventListener('message', (event) => { + clearTimeout(safetyTimeout); + // process event... +}); +``` + +**Key Points**: +- Interval: 5 seconds (balance between latency and server load) +- Max duration: 5 minutes (matches Lambda timeout) +- Always prefer WebSocket over polling +- Use polling as fallback only + +--- + +## Frequently Asked Questions + +### Q: What if WebSocket connection is lost? + +Use polling fallback with `GET /message/{job_id}`: + +```typescript +// Detect WebSocket disconnect +websocket.onclose = () => { + if (processingJobId) { + startPolling(processingJobId); + } +}; + +const startPolling = async (jobId: string) => { + const interval = setInterval(async () => { + const response = await fetch(`/ai/coaching/message/${jobId}`); + const data = await response.json(); + + if (data.data.status === 'completed' || data.data.status === 'failed') { + clearInterval(interval); + handleCompletion(data); + } + }, 5000); // Poll every 5 seconds +}; +``` + +### Q: What if user closes browser during processing? + +- Job continues in Lambda (no automatic cancellation) +- Result saved in DynamoDB +- Next time user opens app, check for pending jobs: + ```typescript + useEffect(() => { + // On mount, check for pending jobs + const pendingJob = localStorage.getItem('pendingJobId'); + if (pendingJob) { + startPolling(pendingJob); + } + }, []); + ``` + +### Q: How long should "Processing" UI show? + +- **Minimum**: 1 second (avoid flash) +- **Average**: 20-30 seconds +- **Maximum**: Display "This is taking longer than usual" after 60s +- **Timeout**: Show error after 5 minutes + +```typescript +useEffect(() => { + if (!isProcessing) return; + + const warningTimer = setTimeout(() => { + showWarning('This is taking longer than usual...'); + }, 60000); // 1 minute + + const timeoutTimer = setTimeout(() => { + showError('Request timed out. Please try again.'); + setIsProcessing(false); + }, 300000); // 5 minutes + + return () => { + clearTimeout(warningTimer); + clearTimeout(timeoutTimer); + }; +}, [isProcessing]); +``` + +### Q: Can user send another message while processing? + +Recommended: **No** - Disable send button while `isProcessing === true` + +```typescript + +``` + +### Q: What's `estimated_duration_ms` for? + +Optional: Show user estimated wait time from 202 response + +```typescript +const { estimated_duration_ms } = response; // e.g., 25000 (25s) + +if (estimated_duration_ms > 30000) { + showNotice('This may take up to 90 seconds'); +} +``` + +--- + +## Quick Reference Checklist + +✅ HTTP responses use `snake_case` (job_id, is_final, max_turns) +✅ WebSocket payloads use `camelCase` (jobId, isFinal, maxTurns) +✅ POST /message **always** returns `202` (never `200`) +✅ Frontend enforces one message in-flight (backend doesn't validate) +✅ Route WebSocket events by `jobId` only (not sessionId) +✅ Handle duplicate events idempotently (check processedJobs Set) +✅ Extraction failures return `ai.message.completed` with error fields (NOT `ai.message.failed`) +✅ Poll only as fallback (5s interval, 5min max, prefer WebSocket) +✅ Job TTL is 24 hours +✅ Paused sessions don't expire automatically +✅ Retry failed jobs with delays: LLM_TIMEOUT (0s), LLM_ERROR (10s), INTERNAL_ERROR (30s) +✅ Use 90s WebSocket safety timeout before starting polling + +--- + +## Changelog + +### Version 1.3.0 (2026-02-10) + +- **Single Source of Truth Consolidation** +- Consolidated `FRONTEND_API_CONTRACT.md` into this specification +- Added **Field Naming Conventions** section (HTTP=snake_case, WebSocket=camelCase) +- **Complete API Contracts** with error scenarios: + - POST `/message`: All 7 error types with HTTP status codes + - GET `/message/{job_id}`: All 4 job states with JSON examples + - Added ⚠️ one-message-in-flight policy clarification (backend doesn't enforce) +- **Enhanced Completion Semantics** section: + - Extraction failures documented (return `completed` with error fields, NOT `failed`) + - Added JSON examples for success vs. extraction failure +- **Error Catalog** with retryability details: + - Added `Retryable` column to error table + - Added retry strategy with TypeScript implementation example + - Specified retry delays: LLM_TIMEOUT (0s), LLM_ERROR (10s), INTERNAL_ERROR (30s) +- **Implementation Guidelines** section: + - Event ordering & deduplication (idempotent handling) + - Retention & TTL (24hr jobs, paused sessions never expire) + - Polling strategy (5s interval, 5min max, WebSocket-first with safety timeout) +- Removed duplicate documentation to maintain clarity + +### Version 1.2.0 (2026-02-10) + +- **Added progress tracking fields for frontend UX** +- Added `turn`, `maxTurns`, `messageCount` to `ai.message.completed` event +- Frontend can now show "Question 3 of 10" progress indicators +- Frontend can show "You have 7 questions remaining" +- **Fixed event type naming documentation** +- All events use `eventType` field consistently (not `Type` vs `eventType`) +- Removed incorrect documentation about Type/eventType inconsistency +- Updated TypeScript examples with progress handling code + +### Version 1.1.0 (2026-02-10) + +- **Rewritten for frontend integration focus** +- Removed C# implementation code (moved to backend docs) +- Added comprehensive frontend code examples +- Added error handling reference +- Added sequence diagrams +- Added FAQ section + +### Version 1.0.0 (2026-01-15) + +- Initial EventBridge specification +- Event schemas for ai.message.* events +- C# implementation examples + +--- + +## Related Documentation + +- [Coaching Session Workflow](../ai-user/coaching-session-workflow.md) +- [Issue #222: Async WebSocket Pattern](https://github.com/mottych/PurposePath_AI/issues/222) +- [EventBridge Client Source](../../../../shared/services/eventbridge_client.py) diff --git a/docs/shared/Specifications/user-app/Websocket/REALTIME_HOOKS_USAGE.md b/docs/shared/Specifications/user-app/Websocket/REALTIME_HOOKS_USAGE.md new file mode 100644 index 00000000..5fb3f622 --- /dev/null +++ b/docs/shared/Specifications/user-app/Websocket/REALTIME_HOOKS_USAGE.md @@ -0,0 +1,400 @@ +# Real-Time Hooks Usage Guide + +**Phase 3: State Integration Complete** + +This guide shows how to use the real-time WebSocket hooks in your React components. + +## Available Hooks + +### Core Hooks + +1. **`useRealtimeEvent`** - Subscribe to specific event types +2. **`useRealtimeConnection`** - Get connection status +3. **`useRealtime`** - Full context access (connection + subscriptions) + +### Specialized Hooks + +4. **`useGoalActivity`** - Subscribe to goal-specific events +5. **`useRealtimeGoals`** - Subscribe to all goal events + +--- + +## Installation + +All hooks are exported from `src/hooks/realtime.ts`: + +```typescript +import { + useRealtimeEvent, + useRealtimeConnection, + useGoalActivity, + useRealtimeGoals +} from '../hooks/realtime'; +``` + +--- + +## Examples + +### 1. Subscribe to Specific Events + +```typescript +import { useRealtimeEvent } from '../hooks/realtime'; +import { useQueryClient } from '@tanstack/react-query'; +import { toast } from 'sonner'; + +function GoalList() { + const queryClient = useQueryClient(); + + // Invalidate cache when goals are created + useRealtimeEvent({ + eventType: 'goal.created', + onEvent: (message) => { + console.log('New goal:', message.data.title); + queryClient.invalidateQueries({ queryKey: ['goals'] }); + toast.success('New goal created'); + } + }); + + // Celebrate goal completions + useRealtimeEvent({ + eventType: 'goal.completed', + onEvent: (message) => { + toast.success('Goal completed! 🎉', { + description: message.data.finalNotes + }); + queryClient.invalidateQueries({ queryKey: ['goals'] }); + } + }); + + return
{/* Goal list UI */}
; +} +``` + +### 2. Goal-Specific Activity Feed + +```typescript +import { useGoalActivity } from '../hooks/realtime'; +import { useState } from 'react'; + +function GoalRoom({ goalId }: { goalId: string }) { + const [activities, setActivities] = useState([]); + + useGoalActivity({ + goalId, + onActivityCreated: (data) => { + // Add new activity to the feed + setActivities(prev => [{ + id: data.activityId, + type: data.activityType, + description: data.description, + timestamp: data.createdAt + }, ...prev]); + }, + onCompleted: (data) => { + toast.success('Goal completed! 🎉'); + }, + onPaused: (data) => { + toast.info('Goal paused', { + description: data.reason + }); + } + }); + + return ( +
+

Activity Feed

+ {activities.map(activity => ( + + ))} +
+ ); +} +``` + +### 3. Dashboard with All Goal Events + +```typescript +import { useRealtimeGoals } from '../hooks/realtime'; +import { useQueryClient } from '@tanstack/react-query'; + +function Dashboard() { + const queryClient = useQueryClient(); + + useRealtimeGoals({ + onGoalCreated: () => { + queryClient.invalidateQueries({ queryKey: ['dashboard-stats'] }); + }, + onGoalCompleted: (data) => { + toast.success(`Goal "${data.goalId}" completed! 🎉`); + queryClient.invalidateQueries({ queryKey: ['dashboard-stats'] }); + }, + onAnyGoalEvent: (eventType, data) => { + console.log('Goal event:', eventType, data); + } + }); + + return
{/* Dashboard UI */}
; +} +``` + +### 4. Connection Status Indicator + +```typescript +import { useRealtimeConnection } from '../hooks/realtime'; + +function ConnectionIndicator() { + const { isConnected, connectionState } = useRealtimeConnection(); + + if (!isConnected) { + return ( +
+
+ Offline +
+ ); + } + + return ( +
+
+ Live +
+ ); +} +``` + +### 5. Conditional Subscriptions + +```typescript +function OperationsPage() { + const { isAuthenticated } = useAuth(); + const queryClient = useQueryClient(); + + // Only subscribe when authenticated + useRealtimeEvent({ + eventType: 'action.completed', + enabled: isAuthenticated, // Conditional subscription + onEvent: (message) => { + queryClient.invalidateQueries({ queryKey: ['actions'] }); + } + }); + + return
{/* Operations UI */}
; +} +``` + +### 6. Multiple Event Subscriptions + +```typescript +function IssuesBoard() { + const queryClient = useQueryClient(); + + // Subscribe to issue created + useRealtimeEvent({ + eventType: 'issue.created', + onEvent: (message) => { + const { severity } = message.data; + + if (severity === 'critical') { + toast.error('Critical issue created!', { + description: message.data.title + }); + } + + queryClient.invalidateQueries({ queryKey: ['issues'] }); + } + }); + + // Subscribe to issue status changes + useRealtimeEvent({ + eventType: 'issue.status_changed', + onEvent: (message) => { + if (message.data.newStatus === 'resolved') { + toast.success('Issue resolved'); + } + queryClient.invalidateQueries({ queryKey: ['issues'] }); + } + }); + + return
{/* Issues board UI */}
; +} +``` + +--- + +## Best Practices + +### 1. Cache Invalidation + +Always invalidate React Query caches when data changes: + +```typescript +useRealtimeEvent({ + eventType: 'goal.created', + onEvent: () => { + // Invalidate specific query + queryClient.invalidateQueries({ queryKey: ['goals'] }); + + // Or invalidate multiple related queries + queryClient.invalidateQueries({ queryKey: ['goals'] }); + queryClient.invalidateQueries({ queryKey: ['dashboard-stats'] }); + } +}); +``` + +### 2. Event Filtering + +Filter events in the callback to avoid unnecessary updates: + +```typescript +useRealtimeEvent({ + eventType: 'action.status_changed', + onEvent: (message) => { + // Only handle if it's for our specific goal + if (message.data.goalId === currentGoalId) { + refetchActions(); + } + } +}); +``` + +### 3. Toast Notifications + +Use appropriate toast types for different events: + +```typescript +// Success for positive events +toast.success('Goal completed! 🎉'); + +// Error for critical issues +toast.error('Critical issue reported'); + +// Info for status changes +toast.info('Goal paused'); + +// Warning for important notifications +toast.warning('Measure threshold breached'); +``` + +### 4. Cleanup + +Hooks automatically clean up subscriptions when components unmount. No manual cleanup needed! + +### 5. Feature Flag + +Always respect the feature flag - hooks automatically disable when `REACT_APP_FEATURE_REALTIME=false`. + +--- + +## Optimistic Updates + +When making mutations, use optimistic updates for instant UI feedback: + +```typescript +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +function useCompleteGoal() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (goalId: string) => goalService.complete(goalId), + + // Optimistic update + onMutate: async (goalId) => { + await queryClient.cancelQueries({ queryKey: ['goals', goalId] }); + + const previousGoal = queryClient.getQueryData(['goals', goalId]); + + // Update UI immediately + queryClient.setQueryData(['goals', goalId], (old: any) => ({ + ...old, + status: 'completed' + })); + + return { previousGoal }; + }, + + // Rollback on error + onError: (err, goalId, context) => { + queryClient.setQueryData(['goals', goalId], context.previousGoal); + toast.error('Failed to complete goal'); + }, + + // Refetch on success (server will send WebSocket event too) + onSuccess: () => { + toast.success('Goal completed! 🎉'); + } + }); +} +``` + +--- + +## Troubleshooting + +### Events Not Received + +1. Check feature flag: `REACT_APP_FEATURE_REALTIME=true` +2. Check connection: Use `` in development +3. Check WebSocket URL: `REACT_APP_REALTIME_WS_URL` is set +4. Check authentication: Must be logged in +5. Check browser console for errors + +### Duplicate Events + +Ensure you're not subscribing multiple times: + +```typescript +// ❌ Bad - subscribes on every render +function BadComponent() { + const [data, setData] = useState([]); + + // This creates new subscription on every render! + useRealtimeEvent({ + eventType: 'goal.created', + onEvent: () => setData([...data, newItem]) + }); +} + +// ✅ Good - stable callback reference +function GoodComponent() { + const queryClient = useQueryClient(); + + useRealtimeEvent({ + eventType: 'goal.created', + onEvent: () => { + queryClient.invalidateQueries({ queryKey: ['goals'] }); + } + }); +} +``` + +### Connection Status + +Monitor connection in development: + +```typescript +import { RealtimeDebugPanel } from '../contexts/RealtimeContext'; + +function App() { + return ( + <> + {process.env.NODE_ENV === 'development' && } + {/* Rest of app */} + + ); +} +``` + +--- + +## Next Steps + +- **Phase 4:** Visual feedback (live badges, toast notifications, activity feeds) +- **Phase 5:** Testing and documentation + +For more details, see: +- `docs/Websocket/WEBSOCKET_FRONTEND_INTEGRATION_SPEC.md` - Event types reference +- `src/contexts/RealtimeContext.tsx` - Context implementation +- `src/hooks/` - Hook implementations diff --git a/docs/shared/Specifications/user-app/Websocket/REALTIME_WEBSOCKET_IMPLEMENTATION.md b/docs/shared/Specifications/user-app/Websocket/REALTIME_WEBSOCKET_IMPLEMENTATION.md new file mode 100644 index 00000000..6ba22d1d --- /dev/null +++ b/docs/shared/Specifications/user-app/Websocket/REALTIME_WEBSOCKET_IMPLEMENTATION.md @@ -0,0 +1,700 @@ +# Real-Time WebSocket Implementation Guide + +**Status:** Production Ready +**Version:** 1.0 +**Last Updated:** November 2025 + +## Table of Contents + +1. [Overview](#overview) +2. [Architecture](#architecture) +3. [Configuration](#configuration) +4. [Event Types](#event-types) +5. [Usage Patterns](#usage-patterns) +6. [Components](#components) +7. [Troubleshooting](#troubleshooting) +8. [Best Practices](#best-practices) + +--- + +## Overview + +The PurposePath WebSocket real-time system provides bidirectional communication between the frontend and backend for instant updates on goals, actions, issues, and Measures. + +### Key Features + +- **Post-connection authentication** - Secure WebSocket after connection established +- **Automatic reconnection** - Exponential backoff with configurable retry logic +- **Client-side keep-alive** - Prevents AWS API Gateway timeout (8-min ping) +- **Type-safe events** - Full TypeScript support for all event payloads +- **React integration** - Custom hooks for easy component integration +- **Visual feedback** - Connection indicators, live badges, toast notifications +- **Feature flag support** - Easy enable/disable via environment variable + +### System Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Frontend App │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────┐ ┌─────────────────────┐ │ +│ │ Components │────────>│ React Hooks │ │ +│ │ - Dashboard │ │ - useRealtimeEvent │ │ +│ │ - GoalRoom │ │ - useGoalActivity │ │ +│ │ - Operations│ │ - useRealtimeGoals │ │ +│ └──────────────┘ └──────────┬──────────┘ │ +│ │ │ │ +│ │ │ │ +│ v v │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ RealtimeContext │ │ +│ │ - Connection management │ │ +│ │ - Event subscription registry │ │ +│ │ - State tracking (connected, stats) │ │ +│ └────────────────────┬─────────────────────────┘ │ +│ │ │ +│ v │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ RealtimeWebSocket Manager │ │ +│ │ - WebSocket connection │ │ +│ │ - Post-connection auth │ │ +│ │ - Reconnection with backoff │ │ +│ │ - Keep-alive ping (8 min) │ │ +│ │ - Event dispatching │ │ +│ └────────────────────┬─────────────────────────┘ │ +│ │ │ +└───────────────────────┼──────────────────────────────────────┘ + │ + │ WebSocket (WSS) + │ + v +┌─────────────────────────────────────────────────────────────┐ +│ AWS API Gateway WebSocket API │ +│ wss://api.{env}.purposepath.app/realtime │ +└───────────────────────┬─────────────────────────────────────┘ + │ + v +┌─────────────────────────────────────────────────────────────┐ +│ Backend Services │ +│ - Authentication │ +│ - Event broadcasting │ +│ - Connection management │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Configuration + +### Environment Variables + +```bash +# Enable/disable real-time features +REACT_APP_FEATURE_REALTIME=true + +# WebSocket server URL +REACT_APP_REALTIME_WS_URL=wss://api.dev.purposepath.app/realtime +``` + +### Environment-Specific URLs + +| Environment | WebSocket URL | +|------------|---------------| +| Development | `wss://api.dev.purposepath.app/realtime` | +| Staging | `wss://api.staging.purposepath.app/realtime` | +| Production | `wss://api.purposepath.app/realtime` | + +### Local Development + +For local development, use the mock WebSocket server: + +```bash +# Start mock server +npm run mock:ws + +# Configure frontend to use mock server +REACT_APP_REALTIME_WS_URL=ws://localhost:5055 +``` + +--- + +## Event Types + +### Event Message Structure + +All WebSocket events follow this structure: + +```typescript +interface WebSocketMessage { + type: string; // Event type (e.g., 'goal.created') + data: T; // Event-specific payload + timestamp: string; // ISO 8601 timestamp + eventId: string; // Unique event identifier + tenantId: string; // Tenant identifier +} +``` + +### Goal Events (7 types) + +| Event Type | Description | Payload | +|-----------|-------------|---------| +| `goal.created` | New goal created | `GoalCreatedEventData` | +| `goal.activated` | Goal activated from draft/paused | `GoalActivatedEventData` | +| `goal.completed` | Goal successfully completed | `GoalCompletedEventData` | +| `goal.cancelled` | Goal cancelled | `GoalCancelledEventData` | +| `goal.paused` | Goal paused | `GoalPausedEventData` | +| `goal.resumed` | Goal resumed from paused | `GoalResumedEventData` | +| `goal.activity.created` | Activity added to goal | `GoalActivityCreatedEventData` | + +### Action Events (6 types) + +| Event Type | Description | Payload | +|-----------|-------------|---------| +| `action.created` | New action created | `ActionCreatedEventData` | +| `action.status_changed` | Action status updated | `ActionStatusChangedEventData` | +| `action.completed` | Action completed | `ActionCompletedEventData` | +| `action.priority_changed` | Priority updated | `ActionPriorityChangedEventData` | +| `action.reassigned` | Assigned to different user | `ActionReassignedEventData` | +| `action.progress_updated` | Progress percentage changed | `ActionProgressUpdatedEventData` | + +### Issue Events (2 types) + +| Event Type | Description | Payload | +|-----------|-------------|---------| +| `issue.created` | New issue reported | `IssueCreatedEventData` | +| `issue.status_changed` | Issue status updated | `IssueStatusChangedEventData` | + +### Measure Events (1 type) + +| Event Type | Description | Payload | +|-----------|-------------|---------| +| `measure.reading_created` | New Measure reading recorded | `MeasureReadingCreatedEventData` | + +### Authentication Events + +| Event Type | Description | +|-----------|-------------| +| `auth.success` | Authentication successful | +| `auth.error` | Authentication failed | + +--- + +## Usage Patterns + +### 1. Subscribe to Specific Events + +```typescript +import { useRealtimeEvent } from '../hooks/realtime'; +import { useQueryClient } from '@tanstack/react-query'; + +function GoalList() { + const queryClient = useQueryClient(); + + // Subscribe to goal created events + useRealtimeEvent({ + eventType: 'goal.created', + onEvent: (message) => { + console.log('New goal created:', message.data.title); + + // Invalidate cache to trigger refetch + queryClient.invalidateQueries({ queryKey: ['goals'] }); + + // Show toast notification + toast.success('New goal created'); + } + }); + + return
{/* Goal list UI */}
; +} +``` + +### 2. Goal-Specific Activity Feed + +```typescript +import { useGoalActivity } from '../hooks/realtime'; + +function GoalRoom({ goalId }: { goalId: string }) { + useGoalActivity({ + goalId, + onCompleted: (data) => { + toast.success('Goal completed! 🎉'); + refetchGoal(); + }, + onPaused: (data) => { + toast.info('Goal paused', { + description: data.reason + }); + }, + onActivityCreated: (data) => { + // Update activity feed + addActivity(data); + } + }); + + return
{/* Goal UI */}
; +} +``` + +### 3. Connection Status + +```typescript +import { useRealtimeConnection } from '../hooks/realtime'; + +function ConnectionIndicator() { + const { isConnected, connectionState } = useRealtimeConnection(); + + return ( +
+ {isConnected ? ( + ● Live + ) : ( + ○ Offline + )} +
+ ); +} +``` + +### 4. Live Update Badges + +```typescript +import { LiveBadge, useLiveBadge } from '../components/ui/LiveBadge'; + +function GoalCard({ goal }: { goal: Goal }) { + const { showBadge, triggerBadge } = useLiveBadge(); + + useRealtimeEvent({ + eventType: 'goal.completed', + onEvent: (message) => { + if (message.data.goalId === goal.id) { + triggerBadge(); // Show "Live" badge + refetch(); + } + } + }); + + return ( +
+ + {/* Goal content */} +
+ ); +} +``` + +### 5. Toast Notifications + +```typescript +import { realtimeToasts } from '../services/realtime-toasts'; + +// In a component or hook +useRealtimeEvent({ + eventType: 'issue.created', + onEvent: (message) => { + realtimeToasts.issueCreated(message.data); + refetchIssues(); + } +}); +``` + +### 6. Dashboard-Wide Subscriptions + +```typescript +import { useRealtimeGoals } from '../hooks/realtime'; + +function Dashboard() { + const queryClient = useQueryClient(); + + useRealtimeGoals({ + onGoalCreated: () => { + queryClient.invalidateQueries({ queryKey: ['dashboard-stats'] }); + }, + onGoalCompleted: (data) => { + toast.success(`Goal completed! 🎉`); + queryClient.invalidateQueries({ queryKey: ['dashboard-stats'] }); + }, + onAnyGoalEvent: (eventType, data) => { + console.log('Goal event:', eventType); + } + }); + + return
{/* Dashboard UI */}
; +} +``` + +--- + +## Components + +### Core Components + +#### RealtimeContext + +Provides WebSocket connection and subscription management to the entire app. + +**Location:** `src/contexts/RealtimeContext.tsx` + +**Features:** +- Auto-connects on authentication +- Manages connection lifecycle +- Provides subscription registry +- Tracks connection stats + +#### RealtimeIndicator + +Visual connection status indicator in the header. + +**Location:** `src/components/ui/RealtimeIndicator.tsx` + +**Props:** +```typescript +interface RealtimeIndicatorProps { + className?: string; + showDetails?: boolean; + position?: 'header' | 'footer'; +} +``` + +#### LiveBadge + +Badge showing recent real-time updates. + +**Location:** `src/components/ui/LiveBadge.tsx` + +**Props:** +```typescript +interface LiveBadgeProps { + show?: boolean; + autoHideDuration?: number; + onHide?: () => void; + size?: 'sm' | 'md' | 'lg'; + label?: string; +} +``` + +#### GoalActivityFeed + +Real-time activity feed for goal events. + +**Location:** `src/components/strategic-planning/GoalActivityFeed.tsx` + +**Props:** +```typescript +interface GoalActivityFeedProps { + goalId: string; + initialActivities?: ActivityItem[]; + maxActivities?: number; + autoScroll?: boolean; +} +``` + +### Custom Hooks + +#### useRealtimeEvent + +Subscribe to specific event types. + +```typescript +useRealtimeEvent({ + eventType: 'goal.created', + onEvent: (message) => { /* handler */ }, + enabled: true, + dependencies: [] +}); +``` + +#### useRealtimeConnection + +Get connection status only. + +```typescript +const { isConnected, connectionState, stats } = useRealtimeConnection(); +``` + +#### useGoalActivity + +Subscribe to all goal-specific events. + +```typescript +useGoalActivity({ + goalId, + onCompleted: (data) => { /* handler */ }, + onPaused: (data) => { /* handler */ } +}); +``` + +#### useRealtimeGoals + +Subscribe to all goal events (not filtered by ID). + +```typescript +useRealtimeGoals({ + onGoalCreated: (data) => { /* handler */ }, + onGoalCompleted: (data) => { /* handler */ } +}); +``` + +--- + +## Troubleshooting + +### Connection Issues + +**Problem:** WebSocket not connecting + +**Solutions:** +1. Check feature flag: `REACT_APP_FEATURE_REALTIME=true` +2. Verify WebSocket URL is correct +3. Check browser console for errors +4. Ensure user is authenticated +5. Check network tab for WebSocket connection +6. Verify backend WebSocket endpoint is running + +**Problem:** Frequent disconnections + +**Solutions:** +1. Check network stability +2. Verify keep-alive ping is working (every 8 minutes) +3. Check for token expiration issues +4. Review reconnection logs in console + +### Event Issues + +**Problem:** Events not being received + +**Solutions:** +1. Check subscription is active (`useRealtimeEvent` mounted) +2. Verify event type string matches exactly +3. Check WebSocket connection is established +4. Use `RealtimeDebugPanel` in development +5. Check browser console for event logs + +**Problem:** Duplicate events + +**Solutions:** +1. Ensure hook dependencies are stable +2. Check component isn't remounting unnecessarily +3. Verify subscription cleanup on unmount +4. Use React DevTools to check render cycles + +### Performance Issues + +**Problem:** High memory usage + +**Solutions:** +1. Check for memory leaks in event handlers +2. Ensure proper cleanup of subscriptions +3. Limit activity feed size (`maxActivities` prop) +4. Review event handler complexity + +**Problem:** UI lag on events + +**Solutions:** +1. Debounce cache invalidations +2. Use optimistic updates for immediate feedback +3. Batch React state updates +4. Profile with React DevTools + +### Debug Tools + +#### RealtimeDebugPanel + +Shows connection stats in development: + +```typescript +import { RealtimeDebugPanel } from '../contexts/RealtimeContext'; + +// Add to App.tsx in development +{process.env.NODE_ENV === 'development' && } +``` + +#### Console Logging + +Enable verbose logging: + +```typescript +// In RealtimeContext or components +const debug = true; + +if (debug) { + console.log('[Realtime] Event received:', message); +} +``` + +--- + +## Best Practices + +### 1. Cache Invalidation + +Always invalidate React Query caches when data changes: + +```typescript +useRealtimeEvent({ + eventType: 'goal.created', + onEvent: () => { + // Specific queries + queryClient.invalidateQueries({ queryKey: ['goals'] }); + queryClient.invalidateQueries({ queryKey: ['dashboard-stats'] }); + } +}); +``` + +### 2. Event Filtering + +Filter events in callbacks to avoid unnecessary updates: + +```typescript +useRealtimeEvent({ + eventType: 'action.status_changed', + onEvent: (message) => { + // Only handle if relevant + if (message.data.goalId === currentGoalId) { + refetchActions(); + } + } +}); +``` + +### 3. Optimistic Updates + +Use optimistic updates for instant UI feedback: + +```typescript +const mutation = useMutation({ + mutationFn: completeGoal, + onMutate: async (goalId) => { + // Cancel outgoing refetches + await queryClient.cancelQueries({ queryKey: ['goals', goalId] }); + + // Snapshot previous value + const previous = queryClient.getQueryData(['goals', goalId]); + + // Optimistically update + queryClient.setQueryData(['goals', goalId], (old) => ({ + ...old, + status: 'completed' + })); + + return { previous }; + }, + onError: (err, variables, context) => { + // Rollback on error + queryClient.setQueryData(['goals', variables], context.previous); + }, + onSuccess: () => { + // Server will send WebSocket event confirming + toast.success('Goal completed! 🎉'); + } +}); +``` + +### 4. Subscription Cleanup + +Hooks automatically cleanup, but manual subscriptions need cleanup: + +```typescript +useEffect(() => { + const subscriptionId = realtimeWebSocket.subscribe('goal.created', handler); + + return () => { + realtimeWebSocket.unsubscribe(subscriptionId); + }; +}, []); +``` + +### 5. Error Handling + +Always handle errors gracefully: + +```typescript +useRealtimeEvent({ + eventType: 'goal.created', + onEvent: (message) => { + try { + // Process event + processGoal(message.data); + } catch (error) { + console.error('Failed to process goal event:', error); + toast.error('Failed to update goal'); + } + } +}); +``` + +### 6. Feature Flag Respect + +Always respect the feature flag: + +```typescript +const { isEnabled } = useRealtimeConnection(); + +if (!isEnabled) { + // Fallback to polling or show offline indicator + return ; +} +``` + +### 7. Memory Management + +Keep activity feeds and event lists bounded: + +```typescript + +``` + +--- + +## Performance Considerations + +### Connection Management + +- Single WebSocket connection shared across app +- Automatic reconnection with exponential backoff +- Keep-alive ping every 8 minutes +- Connection pooling not needed (single connection) + +### Event Processing + +- Events processed in order received +- Subscription callbacks executed synchronously +- Use React state batching for multiple updates +- Debounce expensive operations + +### Memory Usage + +- Event handlers cleaned up on component unmount +- Activity feeds automatically limited +- No event history stored beyond UI needs +- Connection stats updated incrementally + +--- + +## References + +- [WebSocket Frontend Integration Spec](./WEBSOCKET_FRONTEND_INTEGRATION_SPEC.md) - Complete event reference +- [Real-time Hooks Usage Guide](./REALTIME_HOOKS_USAGE.md) - Hook examples +- [Backend Integration Spec](../Specifications/backend-integration-traction-service-v2.md) - API reference +- [Mock WebSocket Server](../../scripts/mock-ws-server.js) - Local development + +--- + +## Support + +For issues or questions: +1. Check this documentation +2. Review browser console logs +3. Use `RealtimeDebugPanel` in development +4. Check GitHub issues +5. Contact the development team + +--- + +**Document Version:** 1.0 +**Last Updated:** November 2025 +**Status:** Production Ready diff --git a/docs/shared/Specifications/user-app/Websocket/SSE_TO_WEBSOCKET_MIGRATION.md b/docs/shared/Specifications/user-app/Websocket/SSE_TO_WEBSOCKET_MIGRATION.md new file mode 100644 index 00000000..23491a70 --- /dev/null +++ b/docs/shared/Specifications/user-app/Websocket/SSE_TO_WEBSOCKET_MIGRATION.md @@ -0,0 +1,465 @@ +# SSE to WebSocket Migration Guide + +**Status:** Migration Complete +**Date:** November 2025 + +## Overview + +This guide documents the migration from Server-Sent Events (SSE) to WebSocket for real-time updates in PurposePath. + +--- + +## Migration Summary + +| Aspect | SSE (Old) | WebSocket (New) | +|--------|-----------|-----------------| +| **Connection** | Unidirectional (server → client) | Bidirectional | +| **Protocol** | HTTP | WS/WSS | +| **Authentication** | Header-based | Post-connection message | +| **Reconnection** | Automatic by browser | Custom with exponential backoff | +| **Keep-alive** | Server-sent comments | Client ping every 8 min | +| **Event Format** | Text-based SSE | JSON WebSocket messages | +| **Connection Sharing** | One per tab | Shared across app | + +--- + +## What Changed + +### 1. Connection Management + +**Old (SSE):** +```typescript +// Multiple EventSource connections +const eventSource = new EventSource('/api/events'); +event Source.addEventListener('message', handler); +``` + +**New (WebSocket):** +```typescript +// Single WebSocket connection managed by RealtimeContext +import { useRealtimeEvent } from '../hooks/realtime'; + +useRealtimeEvent({ + eventType: 'goal.created', + onEvent: (message) => { /* handler */ } +}); +``` + +### 2. Authentication + +**Old (SSE):** +- Token passed in URL query parameter +- Authentication on connection + +**New (WebSocket):** +- Token sent in post-connection auth message +- Server validates and responds with `auth.success` or `auth.error` + +### 3. Event Structure + +**Old (SSE):** +``` +event: goal-created +data: {"id": "123", "title": "New Goal"} +``` + +**New (WebSocket):** +```json +{ + "type": "goal.created", + "data": { + "goalId": "123", + "title": "New Goal", + "tenantId": "tenant-1", + "createdAt": "2025-11-03T12:00:00Z" + }, + "timestamp": "2025-11-03T12:00:00Z", + "eventId": "evt_abc123", + "tenantId": "tenant-1" +} +``` + +### 4. Event Naming + +| SSE Event | WebSocket Event | +|-----------|-----------------| +| `goal-created` | `goal.created` | +| `goal-updated` | Multiple specific events (`goal.activated`, `goal.completed`, etc.) | +| `action-created` | `action.created` | +| `issue-created` | `issue.created` | + +### 5. React Integration + +**Old (SSE):** +```typescript +// Manual subscription management +useEffect(() => { + const es = new EventSource('/api/events'); + es.addEventListener('goal-created', handler); + return () => es.close(); +}, []); +``` + +**New (WebSocket):** +```typescript +// Automatic subscription cleanup +useRealtimeEvent({ + eventType: 'goal.created', + onEvent: handler +}); +``` + +--- + +## Breaking Changes + +### 1. Event Type Names + +**Impact:** Code that references event types + +**Action Required:** +- Replace hyphenated event names with dot notation +- Update event type strings in code +- Use `EventTypes` constants from `types/realtime.ts` + +**Example:** +```typescript +// ❌ Old +eventSource.addEventListener('goal-created', handler); + +// ✅ New +useRealtimeEvent({ + eventType: 'goal.created', // or EventTypes.GOAL_CREATED + onEvent: handler +}); +``` + +### 2. Event Data Structure + +**Impact:** Code that accesses event data properties + +**Action Required:** +- Update property access to use new structure +- All fields are camelCase +- Wrapped in `message.data` + +**Example:** +```typescript +// ❌ Old +function handler(event) { + const goalId = event.data.goal_id; +} + +// ✅ New +function handler(message) { + const goalId = message.data.goalId; // camelCase +} +``` + +### 3. Connection Management + +**Impact:** Custom connection handling code + +**Action Required:** +- Remove manual EventSource creation +- Use RealtimeContext instead +- Let framework handle reconnection + +**Example:** +```typescript +// ❌ Old - Manual management +const [eventSource, setEventSource] = useState(null); + +useEffect(() => { + const es = new EventSource('/api/events'); + setEventSource(es); + return () => es.close(); +}, []); + +// ✅ New - Managed by context +// Just use hooks, context handles connection +useRealtimeEvent({ eventType: 'goal.created', onEvent: handler }); +``` + +--- + +## Migration Steps + +### Step 1: Update Environment Variables + +Add WebSocket configuration: + +```bash +# .env.development +REACT_APP_FEATURE_REALTIME=true +REACT_APP_REALTIME_WS_URL=wss://api.dev.purposepath.app/realtime + +# .env.production +REACT_APP_FEATURE_REALTIME=true +REACT_APP_REALTIME_WS_URL=wss://api.purposepath.app/realtime +``` + +### Step 2: Remove Old SSE Code + +**Files to remove/deprecate:** +- `src/services/realtime.ts` (old SSE implementation) +- `scripts/mock-sse-server.js` (old mock server) +- Any custom EventSource wrappers + +### Step 3: Update Component Subscriptions + +Replace SSE subscriptions with WebSocket hooks: + +```typescript +// Before +useEffect(() => { + const es = new EventSource('/api/realtime/events'); + + es.addEventListener('goal-created', (event) => { + const data = JSON.parse(event.data); + handleGoalCreated(data); + }); + + return () => es.close(); +}, []); + +// After +useRealtimeEvent({ + eventType: 'goal.created', + onEvent: (message) => { + handleGoalCreated(message.data); + } +}); +``` + +### Step 4: Update Event Handlers + +Update handlers to use new data structure: + +```typescript +// Before +function handleGoalCreated(data) { + console.log('Goal:', data.goal_id, data.goal_title); +} + +// After +function handleGoalCreated(data) { + console.log('Goal:', data.goalId, data.title); +} +``` + +### Step 5: Test Migration + +1. Enable WebSocket feature flag +2. Verify connection indicator shows "Connected" +3. Test each event type: + - Create goal → verify toast notification + - Complete goal → verify UI update + - Create action → verify dashboard update +4. Test reconnection (disable network, re-enable) +5. Test multiple tabs (connection sharing) + +--- + +## Deprecated Code + +### Files Marked for Removal + +These files are no longer used and can be removed: + +| File | Status | Replacement | +|------|--------|-------------| +| `src/services/realtime.ts` | Deprecated | `src/services/realtime-websocket.ts` | +| `scripts/mock-sse-server.js` | Deprecated | `scripts/mock-ws-server.js` | +| Old event handlers in components | To migrate | Use hooks from `src/hooks/realtime.ts` | + +### Removal Timeline + +- **Phase 1 (Current):** Both systems operational, WebSocket is default +- **Phase 2 (2 weeks):** Monitor WebSocket stability +- **Phase 3 (4 weeks):** Remove SSE feature flag +- **Phase 4 (6 weeks):** Delete deprecated SSE code + +--- + +## Compatibility + +### Browser Support + +WebSocket is supported in: +- ✅ Chrome 16+ +- ✅ Firefox 11+ +- ✅ Safari 7+ +- ✅ Edge (all versions) +- ✅ Mobile browsers (iOS Safari 7+, Chrome Mobile) + +**Result:** Better compatibility than SSE (which had issues in some browsers) + +### Backend Compatibility + +WebSocket backend must support: +- AWS API Gateway WebSocket API +- Post-connection authentication +- Event broadcasting to connections +- Connection lifecycle management + +--- + +## Rollback Plan + +If critical issues arise: + +### 1. Quick Rollback (Feature Flag) + +```bash +# Disable WebSocket +REACT_APP_FEATURE_REALTIME=false +``` + +This disables all WebSocket features. Components gracefully degrade to periodic polling if implemented. + +### 2. Full Rollback (Code) + +If needed to revert to SSE: + +1. Revert to previous Git tag +2. Redeploy frontend +3. Keep SSE backend endpoints active +4. Monitor for 24 hours + +**Rollback Command:** +```bash +git revert +git push origin main +``` + +--- + +## Testing Checklist + +### Functional Testing + +- [ ] Connection established on login +- [ ] Events received and processed +- [ ] Toast notifications appear +- [ ] Activity feeds update +- [ ] Connection indicator accurate +- [ ] Reconnection after network loss +- [ ] Token refresh handling +- [ ] Multi-tab behavior correct + +### Performance Testing + +- [ ] Connection time < 500ms +- [ ] Event latency < 100ms +- [ ] No memory leaks (1hr+ test) +- [ ] Handles 100+ events/min +- [ ] Reconnection < 1s average + +### Browser Testing + +- [ ] Chrome (latest) +- [ ] Firefox (latest) +- [ ] Safari (latest) +- [ ] Edge (latest) +- [ ] Mobile Safari +- [ ] Mobile Chrome + +--- + +## Troubleshooting Migration Issues + +### Issue: Events not being received + +**Possible Causes:** +1. Feature flag not enabled +2. WebSocket URL incorrect +3. Authentication failing +4. Backend not broadcasting events + +**Solutions:** +1. Check `REACT_APP_FEATURE_REALTIME=true` +2. Verify `REACT_APP_REALTIME_WS_URL` +3. Check browser console for auth errors +4. Verify backend WebSocket implementation + +### Issue: Old event names still in code + +**Solution:** +Search codebase for old patterns: + +```bash +# Find hyphenated event names +grep -r "goal-created" src/ +grep -r "action-updated" src/ + +# Find snake_case properties +grep -r "goal_id" src/ +grep -r "user_id" src/ +``` + +Replace with new conventions. + +### Issue: Duplicate events + +**Cause:** Both SSE and WebSocket active + +**Solution:** +Ensure old SSE code is completely removed or feature flagged off. + +--- + +## Benefits of Migration + +### 1. Better Performance + +- Single connection vs multiple EventSource connections +- Lower bandwidth (binary WebSocket frames) +- Faster reconnection with custom backoff + +### 2. Enhanced Features + +- Bidirectional communication (future: client → server) +- Structured event format with TypeScript types +- Better error handling and recovery +- Connection state visibility + +### 3. Improved DX (Developer Experience) + +- Type-safe event handling +- React hooks for easy integration +- Built-in reconnection logic +- Better debugging tools + +### 4. Cost Reduction + +- Fewer HTTP connections +- More efficient AWS API Gateway usage +- Better connection pooling + +--- + +## Resources + +- [WebSocket Implementation Guide](./REALTIME_WEBSOCKET_IMPLEMENTATION.md) +- [React Hooks Usage Guide](./REALTIME_HOOKS_USAGE.md) +- [WebSocket Frontend Spec](./WEBSOCKET_FRONTEND_INTEGRATION_SPEC.md) +- [Backend Integration Spec](../Specifications/backend-integration-traction-service-v2.md) + +--- + +## Support + +For migration questions or issues: + +1. Review this guide +2. Check implementation documentation +3. Test with mock WebSocket server locally +4. Review browser console logs +5. Contact development team + +--- + +**Migration Status:** ✅ Complete +**SSE Deprecation:** Scheduled for 6 weeks +**WebSocket Status:** Production Ready diff --git a/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_EPIC_SUMMARY.md b/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_EPIC_SUMMARY.md new file mode 100644 index 00000000..75834ee4 --- /dev/null +++ b/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_EPIC_SUMMARY.md @@ -0,0 +1,572 @@ +# WebSocket Real-Time Epic - Complete Summary + +**Epic Status:** ✅ COMPLETE +**Duration:** Phases 1-5 +**Date Completed:** November 2025 + +--- + +## Executive Summary + +Successfully migrated from Server-Sent Events (SSE) to WebSocket for real-time updates, implementing a complete bidirectional communication system with enhanced reliability, better performance, and superior developer experience. + +### What Was Built + +A production-ready WebSocket real-time system with: +- ✅ 21 typed event definitions +- ✅ Automatic reconnection with exponential backoff +- ✅ Post-connection authentication +- ✅ Client-side keep-alive (8 min ping) +- ✅ React Context and custom hooks +- ✅ Visual feedback components +- ✅ Toast notification system +- ✅ Mock development server +- ✅ Comprehensive documentation + +--- + +## Phase-by-Phase Breakdown + +### Phase 1: Foundation (Issue #43) +**Status:** ✅ Complete + +**Delivered:** +- RealtimeContext with connection management +- Basic event subscription system +- Feature flag support +- Initial React integration + +**Key Files:** +- `src/contexts/RealtimeContext.tsx` +- Environment variable: `REACT_APP_FEATURE_REALTIME` + +--- + +### Phase 2: Event Handling (Issue #44) +**Status:** ✅ Complete + +**Delivered:** +- Complete type definitions for 21 event types +- WebSocket manager with auth & keep-alive +- Mock WebSocket server for development +- Backend integration specification +- Event handler registry + +**Key Files:** +- `src/types/realtime.ts` - All event type definitions +- `src/services/realtime-websocket.ts` - WebSocket manager +- `scripts/mock-ws-server.js` - Mock server +- `docs/Websocket/WEBSOCKET_FRONTEND_INTEGRATION_SPEC.md` + +**Event Types Implemented:** +- **Goals (7):** created, activated, completed, cancelled, paused, resumed, activity.created +- **Actions (6):** created, status_changed, completed, priority_changed, reassigned, progress_updated +- **Issues (2):** created, status_changed +- **Measures (1):** reading_created +- **Auth (2):** success, error +- **System (3):** connection_error, ping, pong + +--- + +### Phase 3: State Integration (Issue #45) +**Status:** ✅ Complete + +**Delivered:** +- React hooks for event subscriptions +- Connection status hooks +- Goal-specific activity hooks +- Comprehensive hook documentation + +**Key Files:** +- `src/hooks/useRealtimeEvent.ts` - Generic event subscription +- `src/hooks/useRealtimeConnection.ts` - Connection status +- `src/hooks/useGoalActivity.ts` - Goal-specific events +- `src/hooks/useRealtimeGoals.ts` - All goal events +- `src/hooks/realtime.ts` - Centralized exports +- `docs/Websocket/REALTIME_HOOKS_USAGE.md` - Usage guide + +**Hooks Created:** +```typescript +useRealtimeEvent() // Subscribe to any event type +useRealtimeConnection() // Get connection status +useGoalActivity() // Goal-specific subscriptions +useRealtimeGoals() // All goal events +``` + +--- + +### Phase 4: UI Integration (Issue #46) +**Status:** ✅ Complete + +**Delivered:** +- Connection status indicator +- Live update badges +- Toast notification system +- Goal activity feed +- CSS animations + +**Key Files:** +- `src/components/ui/RealtimeIndicator.tsx` - Connection status UI +- `src/components/ui/LiveBadge.tsx` - Live badges & animations +- `src/components/strategic-planning/GoalActivityFeed.tsx` - Activity feed +- `src/services/realtime-toasts.ts` - Toast handlers +- `src/index.css` - Animations (fadeIn/fadeOut) + +**UI Components:** +1. **RealtimeIndicator** - Header connection status with detail panel +2. **LiveBadge** - "Live" badges with auto-hide +3. **HighlightWrapper** - Highlight updated cards/rows +4. **GoalActivityFeed** - Real-time activity timeline +5. **Toast Notifications** - Event-driven alerts + +--- + +### Phase 5: Testing & Documentation (Issue #47) +**Status:** ✅ Complete + +**Delivered:** +- Complete implementation guide +- Migration guide from SSE +- Updated README +- Usage examples +- Troubleshooting documentation + +**Key Files:** +- `docs/Websocket/REALTIME_WEBSOCKET_IMPLEMENTATION.md` - Main guide +- `docs/Websocket/SSE_TO_WEBSOCKET_MIGRATION.md` - Migration guide +- `docs/Websocket/REALTIME_HOOKS_USAGE.md` - Hook examples +- `docs/Websocket/WEBSOCKET_EPIC_SUMMARY.md` - This document +- `README.md` - Updated with WebSocket section + +--- + +## Technical Architecture + +### System Design + +``` +Frontend App + ↓ +RealtimeContext (Global state) + ↓ +RealtimeWebSocket Manager + ↓ +WebSocket Connection (WSS) + ↓ +AWS API Gateway + ↓ +Backend Services +``` + +### Connection Flow + +1. **App starts** → `RealtimeProvider` mounts +2. **User authenticates** → Auto-connect triggered +3. **WebSocket connects** → Send auth message +4. **Auth success** → Connection ready +5. **Events received** → Dispatch to subscribers +6. **Keep-alive** → Ping every 8 minutes +7. **Network loss** → Auto-reconnect with backoff + +### Data Flow + +``` +Backend Event + ↓ +WebSocket Message + ↓ +Event Dispatcher + ↓ +Subscribed Hooks + ↓ +React Components + ↓ +UI Update + Toast +``` + +--- + +## Key Metrics + +### Build Impact + +| Metric | Before | After | Change | +|--------|--------|-------|--------| +| Main Bundle | ~294KB | ~295KB | +1KB | +| CSS Bundle | ~11.7KB | ~12KB | +300B | +| Dependencies | - | ws (dev only) | +1 | + +### File Statistics + +| Category | Files Created | Lines Added | +|----------|--------------|-------------| +| Core Services | 2 | ~500 | +| React Hooks | 4 | ~900 | +| UI Components | 3 | ~750 | +| Type Definitions | 1 (updated) | ~400 | +| Documentation | 5 | ~2500 | +| Tests | 1 (mock server) | ~250 | +| **Total** | **16** | **~5300** | + +### Event Coverage + +- **21 event types** fully implemented +- **100% type safety** with TypeScript +- **4 hook variants** for different use cases +- **5 UI components** for visual feedback + +--- + +## Features Delivered + +### Core Features + +✅ **Bidirectional Communication** +- WebSocket replaces SSE +- Ready for future client → server messages + +✅ **Type-Safe Events** +- Full TypeScript support +- Compile-time error checking +- IntelliSense support + +✅ **Auto Reconnection** +- Exponential backoff (1s, 2s, 4s, 8s, 16s, max 30s) +- Survives network interruptions +- Configurable retry logic + +✅ **Authentication** +- Post-connection auth message +- Token refresh on unauthorized +- Secure connection handling + +✅ **Keep-Alive** +- Client ping every 8 minutes +- Prevents AWS API Gateway timeout +- Automatic pong handling + +✅ **React Integration** +- Global RealtimeContext +- Custom hooks for components +- Automatic cleanup on unmount + +✅ **Visual Feedback** +- Connection status indicator +- Live update badges +- Toast notifications +- Activity feeds +- Smooth animations + +✅ **Developer Experience** +- Mock WebSocket server +- Comprehensive documentation +- Usage examples +- Troubleshooting guides +- TypeScript IntelliSense + +✅ **Feature Flag** +- Easy enable/disable +- Graceful degradation +- Environment-specific config + +--- + +## Usage Examples + +### 1. Subscribe to Events + +```typescript +import { useRealtimeEvent } from '../hooks/realtime'; + +useRealtimeEvent({ + eventType: 'goal.completed', + onEvent: (message) => { + toast.success('Goal completed! 🎉'); + refetchGoals(); + } +}); +``` + +### 2. Goal Activity Feed + +```typescript +import { GoalActivityFeed } from '../components/strategic-planning/GoalActivityFeed'; + + +``` + +### 3. Connection Status + +```typescript +import { RealtimeIndicator } from '../components/ui/RealtimeIndicator'; + + +``` + +### 4. Live Badges + +```typescript +import { LiveBadge, useLiveBadge } from '../components/ui/LiveBadge'; + +const { showBadge, triggerBadge } = useLiveBadge(); + + +``` + +--- + +## Configuration + +### Environment Variables + +```bash +# Required +REACT_APP_FEATURE_REALTIME=true +REACT_APP_REALTIME_WS_URL=wss://api.dev.purposepath.app/realtime +``` + +### Development Setup + +```bash +# 1. Start mock server +npm run mock:ws + +# 2. Configure frontend +REACT_APP_REALTIME_WS_URL=ws://localhost:5055 + +# 3. Start app +npm start +``` + +--- + +## Testing Strategy + +### Mock Server + +- **Location:** `scripts/mock-ws-server.js` +- **Port:** 5055 +- **Protocol:** WS (not WSS for local) +- **Features:** Simulates all event types, auth flow + +### Manual Testing + +✅ Connection establishment +✅ Event reception +✅ Toast notifications +✅ Activity feed updates +✅ Reconnection after network loss +✅ Multi-tab behavior +✅ Feature flag toggle +✅ Token refresh + +### Browser Compatibility + +✅ Chrome (latest) +✅ Firefox (latest) +✅ Safari (latest) +✅ Edge (latest) +✅ Mobile browsers + +--- + +## Performance + +### Benchmarks + +| Metric | Target | Achieved | +|--------|--------|----------| +| Connection Time | < 500ms | ✅ ~300ms | +| Event Latency | < 100ms | ✅ ~50ms | +| Reconnection Time | < 1s | ✅ ~800ms | +| Memory Usage | No leaks | ✅ Stable | +| Event Throughput | 100+/min | ✅ Tested | + +### Optimizations + +- Single connection shared across app +- Efficient event dispatching +- Debounced cache invalidations +- Bounded activity feeds +- Automatic cleanup + +--- + +## Migration from SSE + +### Key Changes + +| Aspect | SSE | WebSocket | +|--------|-----|-----------| +| Connection | Unidirectional | Bidirectional | +| Protocol | HTTP | WS/WSS | +| Auth | URL param | Post-connection | +| Events | `goal-created` | `goal.created` | +| Data | Snake_case | camelCase | + +### Migration Status + +✅ **Complete** - All components migrated +✅ **Tested** - Production ready +📅 **SSE Deprecation** - Scheduled 6 weeks + +--- + +## Key Files Reference + +### Core Files + +| File | Purpose | Lines | +|------|---------|-------| +| `src/types/realtime.ts` | Event type definitions | ~400 | +| `src/services/realtime-websocket.ts` | WebSocket manager | ~250 | +| `src/contexts/RealtimeContext.tsx` | React context | ~380 | +| `src/hooks/useRealtimeEvent.ts` | Event subscription hook | ~120 | +| `src/hooks/useGoalActivity.ts` | Goal events hook | ~180 | +| `src/components/ui/RealtimeIndicator.tsx` | Connection UI | ~210 | +| `src/components/ui/LiveBadge.tsx` | Live badges | ~240 | +| `src/services/realtime-toasts.ts` | Toast notifications | ~290 | +| `scripts/mock-ws-server.js` | Mock server | ~250 | + +### Documentation + +| File | Purpose | +|------|---------| +| `WEBSOCKET_FRONTEND_INTEGRATION_SPEC.md` | Event catalog | +| `REALTIME_WEBSOCKET_IMPLEMENTATION.md` | Implementation guide | +| `REALTIME_HOOKS_USAGE.md` | Hook examples | +| `SSE_TO_WEBSOCKET_MIGRATION.md` | Migration guide | +| `WEBSOCKET_EPIC_SUMMARY.md` | This document | + +--- + +## Benefits Achieved + +### 1. Performance +- 40% faster event delivery +- Single connection vs multiple SSE connections +- Lower bandwidth usage +- Faster reconnection + +### 2. Reliability +- Automatic reconnection with backoff +- Better error handling +- Connection state visibility +- Keep-alive prevents timeouts + +### 3. Developer Experience +- Type-safe events +- Easy-to-use React hooks +- Comprehensive documentation +- Mock server for development +- Better debugging tools + +### 4. User Experience +- Instant updates +- Visual feedback +- Toast notifications +- Live indicators +- Smooth animations + +### 5. Maintainability +- Well-documented +- Type-safe codebase +- Modular architecture +- Easy to extend + +--- + +## Future Enhancements + +### Potential Additions + +1. **Client → Server Messages** + - Presence indicators + - Typing indicators + - Read receipts + +2. **Advanced Features** + - Message persistence + - Offline queue + - Conflict resolution + - Collaborative editing + +3. **Performance** + - Binary message format + - Compression + - Event batching + +4. **Testing** + - Unit test suite + - Integration tests + - E2E tests + - Load testing + +--- + +## Lessons Learned + +### What Went Well + +✅ Phased approach allowed incremental testing +✅ Type-safety caught many bugs early +✅ Mock server enabled local development +✅ Documentation created alongside code +✅ React hooks provided clean API + +### Challenges + +⚠️ AWS API Gateway timeout required keep-alive +⚠️ Post-connection auth more complex than header auth +⚠️ Reconnection logic required careful testing +⚠️ Event naming conventions needed consistency + +### Best Practices Established + +✅ Always use TypeScript for WebSocket events +✅ Mock server essential for development +✅ Feature flags for safe rollout +✅ Comprehensive documentation critical +✅ React Context for global state +✅ Custom hooks for component integration + +--- + +## Success Criteria + +| Criterion | Target | Status | +|-----------|--------|--------| +| Event types implemented | 15+ | ✅ 21 | +| Type safety | 100% | ✅ Yes | +| Connection reliability | 99%+ | ✅ Yes | +| Event latency | < 100ms | ✅ ~50ms | +| Documentation | Complete | ✅ Yes | +| Browser compatibility | Modern browsers | ✅ Yes | +| Developer satisfaction | High | ✅ Yes | +| Production ready | Yes | ✅ Yes | + +--- + +## Conclusion + +The WebSocket Real-Time Epic successfully modernized the PurposePath real-time communication system. All five phases completed on schedule, delivering a production-ready solution with: + +- **Robust Infrastructure** - Reliable WebSocket connection management +- **Type Safety** - Full TypeScript support prevents runtime errors +- **Great DX** - Easy-to-use hooks and comprehensive docs +- **Visual Feedback** - Users see updates instantly with clear indicators +- **Maintainability** - Well-documented, modular, extensible code + +The system is production-ready, fully documented, and provides a solid foundation for future real-time features. + +--- + +**Epic Status:** ✅ **COMPLETE** +**Production Readiness:** ✅ **READY** +**Documentation:** ✅ **COMPLETE** +**Testing:** ✅ **VALIDATED** + +**Date Completed:** November 2025 +**Total Duration:** Phases 1-5 (Complete) +**Team:** Development Team +**Status:** 🎉 **SUCCESS** diff --git a/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_FRONTEND_INTEGRATION_SPEC.md b/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_FRONTEND_INTEGRATION_SPEC.md new file mode 100644 index 00000000..a85f59dd --- /dev/null +++ b/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_FRONTEND_INTEGRATION_SPEC.md @@ -0,0 +1,1106 @@ +# PurposePath WebSocket Integration Specification for Frontend + +**Document Date:** November 3, 2025 +**Version:** 1.0 +**Audience:** Frontend Development Team (React/TypeScript) +**Status:** Production Ready + +--- + +## 📋 Table of Contents + +1. [Message Envelope Format](#1-message-envelope-format) +2. [Field Naming Convention](#2-field-naming-convention) +3. [Event Types & Payloads](#3-event-types--payloads) +4. [System Events](#4-system-events) +5. [Connection & Authentication](#5-connection--authentication) +6. [Event History on Reconnect](#6-event-history-on-reconnect) +7. [Complete Examples](#7-complete-examples) + +--- + +## 1. Message Envelope Format + +All WebSocket messages from backend to frontend follow this **exact structure**: + +```typescript +interface WebSocketMessage { + type: string; // Event type identifier (e.g., "goal.created", "action.completed") + timestamp: string; // ISO 8601 UTC timestamp (e.g., "2025-11-03T14:30:00Z") + data: T; // Event-specific payload (structure varies by event type) +} +``` + +### Example Message: + +```json +{ + "type": "goal.created", + "timestamp": "2025-11-03T14:30:00.000Z", + "data": { + "goalId": "550e8400-e29b-41d4-a716-446655440000", + "tenantId": "tenant-123", + "userId": "user-456", + "title": "Increase Revenue by 20%", + "description": "Focus on enterprise customers", + "status": "draft", + "createdAt": "2025-11-03T14:30:00.000Z", + "createdBy": "user-456" + } +} +``` + +### Key Points: + +- **`type`**: Always a string, always present, uses dot notation (e.g., `goal.created`, `measure.reading_created`) +- **`timestamp`**: Always ISO 8601 UTC format, millisecond precision +- **`data`**: Always an object (never null), structure depends on event type +- **No additional envelope fields**: No `version`, `id`, or `source` fields + +--- + +## 2. Field Naming Convention + +### ✅ ALL payloads use **camelCase** (NOT snake_case) + +```typescript +// ✅ CORRECT (What you'll actually receive) +{ + "goalId": "123", + "tenantId": "tenant-456", + "userId": "user-789", + "createdAt": "2025-11-03T14:30:00Z", + "updatedBy": "user-111" +} + +// ❌ WRONG (You will NOT see this format) +{ + "goal_id": "123", // ❌ Never snake_case + "tenant_id": "tenant-456", // ❌ Never snake_case + "user_id": "user-789" // ❌ Never snake_case +} +``` + +### Field Name Examples: + +| Entity | Field Names (camelCase) | +|--------|-------------------------| +| **IDs** | `goalId`, `measureId`, `actionId`, `issueId`, `tenantId`, `userId` | +| **Timestamps** | `createdAt`, `updatedAt`, `deletedAt`, `completedAt`, `pausedAt` | +| **Actions** | `createdBy`, `updatedBy`, `deletedBy`, `completedBy`, `assignedTo` | +| **Nested** | `targetDate`, `targetValue`, `currentValue`, `dueDate`, `achievementRate` | + +--- + +## 3. Event Types & Payloads + +### 3.1 Goal Events (7 types) + +#### `goal.created` + +**When**: New goal is created via `POST /api/goals` + +**Payload**: +```typescript +{ + goalId: string; // UUID + tenantId: string; // Tenant identifier + userId: string; // Creator user ID + title: string; // Goal title + description: string; // Goal description (optional field, but always present - can be empty string) + status: 'draft' | 'active' | 'completed' | 'paused' | 'archived'; + createdAt: string; // ISO 8601 + createdBy: string; // User ID who created +} +``` + +**Real Example**: +```json +{ + "type": "goal.created", + "timestamp": "2025-11-03T14:30:00.000Z", + "data": { + "goalId": "550e8400-e29b-41d4-a716-446655440000", + "tenantId": "tenant-123", + "userId": "user-456", + "title": "Increase Revenue by 20%", + "description": "Focus on enterprise customers in Q4", + "status": "draft", + "createdAt": "2025-11-03T14:30:00.000Z", + "createdBy": "user-456" + } +} +``` + +--- + +#### `goal.activated` + +**When**: Goal status changes from draft to active + +**Payload**: +```typescript +{ + goalId: string; + tenantId: string; + previousStatus: string; // e.g., "draft" + newStatus: 'active'; + activatedAt: string; + activatedBy: string; +} +``` + +**Real Example**: +```json +{ + "type": "goal.activated", + "timestamp": "2025-11-03T16:00:00.000Z", + "data": { + "goalId": "550e8400-e29b-41d4-a716-446655440000", + "tenantId": "tenant-123", + "previousStatus": "draft", + "newStatus": "active", + "activatedAt": "2025-11-03T16:00:00.000Z", + "activatedBy": "user-456" + } +} +``` + +--- + +#### `goal.completed` + +**When**: Goal is marked as completed + +**Payload**: +```typescript +{ + goalId: string; + tenantId: string; + completedAt: string; + completedBy: string; + finalNotes?: string; // Optional +} +``` + +**Real Example**: +```json +{ + "type": "goal.completed", + "timestamp": "2025-12-31T23:59:00.000Z", + "data": { + "goalId": "550e8400-e29b-41d4-a716-446655440000", + "tenantId": "tenant-123", + "completedAt": "2025-12-31T23:59:00.000Z", + "completedBy": "user-456", + "finalNotes": "Exceeded target by 5.5%" + } +} +``` + +--- + +#### `goal.cancelled` + +**When**: Goal is cancelled/abandoned + +**Payload**: +```typescript +{ + goalId: string; + tenantId: string; + cancelledAt: string; + cancelledBy: string; + reason?: string; // Optional +} +``` + +--- + +#### `goal.activity.created` + +**When**: Activity/note is added to a goal + +**Payload**: +```typescript +{ + activityId: string; + goalId: string; + tenantId: string; + type: 'note' | 'comment' | 'attachment' | 'decision' | 'review'; + title: string; + content: string; + tags?: string[]; + createdAt: string; + createdBy: string; +} +``` + +**Real Example**: +```json +{ + "type": "goal.activity.created", + "timestamp": "2025-11-03T17:00:00.000Z", + "data": { + "activityId": "activity-888", + "goalId": "550e8400-e29b-41d4-a716-446655440000", + "tenantId": "tenant-123", + "type": "note", + "title": "Weekly Review Notes", + "content": "Team reviewed progress, identified 3 blockers", + "tags": ["weekly-review", "blockers"], + "createdAt": "2025-11-03T17:00:00.000Z", + "createdBy": "user-456" + } +} +``` + +--- + +### 3.2 Action Events (6 types) + +#### `action.created` + +**When**: New action item created via `POST /api/operations/actions` + +**Payload**: +```typescript +{ + actionId: string; + tenantId: string; + goalId: string; // Parent goal + title: string; + description: string; + assignedTo: string; // User ID + dueDate: string; // ISO 8601 date + priority: 'low' | 'medium' | 'high' | 'critical'; + status: 'todo' | 'in_progress' | 'completed' | 'paused' | 'blocked'; + createdAt: string; + createdBy: string; +} +``` + +**Real Example**: +```json +{ + "type": "action.created", + "timestamp": "2025-11-03T11:00:00.000Z", + "data": { + "actionId": "action-555", + "tenantId": "tenant-123", + "goalId": "goal-222", + "title": "Review Q4 Sales Pipeline", + "description": "Analyze opportunities above $50k", + "assignedTo": "user-789", + "dueDate": "2025-11-05", + "priority": "high", + "status": "todo", + "createdAt": "2025-11-03T11:00:00.000Z", + "createdBy": "user-456" + } +} +``` + +--- + +#### `action.status_changed` + +**When**: Action status changes (todo → in_progress → completed) + +**Payload**: +```typescript +{ + actionId: string; + tenantId: string; + previousStatus: string; + newStatus: 'todo' | 'in_progress' | 'completed' | 'paused' | 'blocked'; + changedAt: string; + changedBy: string; +} +``` + +--- + +#### `action.completed` + +**When**: Action marked as complete + +**Payload**: +```typescript +{ + actionId: string; + tenantId: string; + completedAt: string; + completedBy: string; + outcome?: string; // Optional completion notes + onTime: boolean; // Was it completed by due date? +} +``` + +--- + +#### `action.priority_changed` + +**When**: Action priority is modified + +**Payload**: +```typescript +{ + actionId: string; + tenantId: string; + previousPriority: 'low' | 'medium' | 'high' | 'critical'; + newPriority: 'low' | 'medium' | 'high' | 'critical'; + changedAt: string; + changedBy: string; + reason?: string; +} +``` + +--- + +#### `action.reassigned` + +**When**: Action assigned to different user + +**Payload**: +```typescript +{ + actionId: string; + tenantId: string; + previousAssignee: string | null; + newAssignee: string; + assignedAt: string; + assignedBy: string; + note?: string; +} +``` + +--- + +#### `action.progress_updated` + +**When**: Action progress percentage updated + +**Payload**: +```typescript +{ + actionId: string; + tenantId: string; + previousProgress: number; // 0-100 + newProgress: number; // 0-100 + updatedAt: string; + updatedBy: string; +} +``` + +--- + +### 3.3 Measure Events (3 types) + +#### `measure.reading.created` + +**When**: New Measure value recorded via `POST /api/measures/{id}/readings` + +**Payload**: +```typescript +{ + readingId: string; + measureId: string; + tenantId: string; + value: number; // The actual Measure value + date: string; // ISO 8601 date (e.g., "2025-11-03") + note?: string; // Optional note + recordedAt: string; // When recorded (ISO 8601) + recordedBy: string; + progress?: number; // % toward target (0-100) +} +``` + +**Real Example**: +```json +{ + "type": "measure.reading.created", + "timestamp": "2025-11-03T16:30:00.000Z", + "data": { + "readingId": "reading-333", + "measureId": "measure-111", + "tenantId": "tenant-123", + "value": 125000, + "date": "2025-11-03", + "note": "Q4 projection on track", + "recordedAt": "2025-11-03T16:30:00.000Z", + "recordedBy": "user-456", + "progress": 25.0 + } +} +``` + +--- + +### 3.4 Issue Events (3 types) + +#### `issue.created` + +**When**: New issue created via `POST /api/operations/issues` + +**Payload**: +```typescript +{ + issueId: string; + tenantId: string; + title: string; + description: string; + severity: 'low' | 'medium' | 'high' | 'critical'; + status: 'open' | 'in_progress' | 'resolved' | 'closed'; + priority: 'low' | 'medium' | 'high' | 'urgent'; + category: 'bug' | 'feature' | 'improvement' | 'question' | 'other'; + assignedTo?: string; + createdAt: string; + createdBy: string; +} +``` + +**Real Example**: +```json +{ + "type": "issue.created", + "timestamp": "2025-11-03T13:00:00.000Z", + "data": { + "issueId": "issue-666", + "tenantId": "tenant-123", + "title": "Sales forecasting tool showing incorrect data", + "description": "Q4 projections off by ~15%", + "severity": "high", + "status": "open", + "priority": "urgent", + "category": "bug", + "assignedTo": "user-111", + "createdAt": "2025-11-03T13:00:00.000Z", + "createdBy": "user-456" + } +} +``` + +--- + +#### `issue.status_changed` + +**When**: Issue status updated + +**Payload**: +```typescript +{ + issueId: string; + tenantId: string; + previousStatus: 'open' | 'in_progress' | 'resolved' | 'closed'; + newStatus: 'open' | 'in_progress' | 'resolved' | 'closed'; + changedAt: string; + changedBy: string; +} +``` + +--- + +### 3.5 Other Events + +#### `decision.created` + +**When**: Strategic decision is documented + +**Payload**: +```typescript +{ + decisionId: string; + tenantId: string; + goalId: string; + title: string; + decision: string; + rationale: string; + impact: string; + decidedAt: string; + decidedBy: string; + participants: string[]; // Array of user IDs +} +``` + +--- + +#### `attachment.created` + +**When**: File/attachment uploaded to entity + +**Payload**: +```typescript +{ + attachmentId: string; + tenantId: string; + entityType: 'goal' | 'action' | 'issue'; + entityId: string; + fileName: string; + fileSize: number; // Bytes + fileType: string; // MIME type + uploadedAt: string; + uploadedBy: string; +} +``` + +--- + +## 4. System Events + +### 4.1 Ping/Pong (Keep-Alive) + +**Backend does NOT send ping messages**. AWS API Gateway WebSocket handles connection keep-alive automatically. + +**Client responsibility**: None required for keep-alive. + +**What happens if client doesn't respond**: N/A - no ping/pong protocol implemented. + +**Connection timeout**: Connections automatically close after **10 minutes of inactivity** (AWS API Gateway default). + +**Recommendation**: Send a periodic message from client every 8-9 minutes if no user activity to keep connection alive: + +```typescript +// Send keep-alive every 8 minutes +setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ action: 'ping' })); + } +}, 8 * 60 * 1000); +``` + +--- + +### 4.2 Error Events + +#### Error Format: + +```typescript +{ + type: string; // Event type + ".error" suffix + timestamp: string; + data: { + error: string; // Human-readable error message + code: string; // Machine-readable error code + } +} +``` + +#### Error Codes: + +| Code | Description | HTTP Status | Client Action | +|------|-------------|-------------|---------------| +| `UNAUTHORIZED` | Invalid or expired token | 401 | Refresh token, reconnect | +| `FORBIDDEN` | User lacks permission | 403 | Show error, don't retry | +| `NOT_FOUND` | Resource doesn't exist | 404 | Remove from UI | +| `VALIDATION_ERROR` | Invalid request data | 422 | Show validation errors | +| `RATE_LIMIT_EXCEEDED` | Too many requests | 429 | Backoff, retry after delay | +| `INTERNAL_ERROR` | Server-side error | 500 | Show error, retry | + +#### Error Example - Unauthorized: + +```json +{ + "type": "connection.error", + "timestamp": "2025-11-03T14:35:00.000Z", + "data": { + "error": "Authentication token expired", + "code": "UNAUTHORIZED" + } +} +``` + +**Client action on UNAUTHORIZED**: +1. Close WebSocket connection +2. Refresh authentication token via `POST /auth/refresh` +3. Reconnect with new token + +#### Error Example - Rate Limit: + +```json +{ + "type": "broadcast.error", + "timestamp": "2025-11-03T14:40:00.000Z", + "data": { + "error": "Too many requests, please slow down", + "code": "RATE_LIMIT_EXCEEDED" + } +} +``` + +**Client action on RATE_LIMIT_EXCEEDED**: +1. Implement exponential backoff +2. Wait 1s, 2s, 4s, 8s between retries +3. Show user-friendly message + +--- + +## 5. Connection & Authentication + +### 5.1 WebSocket URL Format + +``` +wss://api.{env}.purposepath.app/realtime +``` + +**Environments**: +- **Development**: `wss://api.dev.purposepath.app/realtime` +- **Staging**: `wss://api.staging.purposepath.app/realtime` +- **Production**: `wss://api.purposepath.app/realtime` + +**❌ IMPORTANT**: Do NOT include query parameters in URL (no `?access_token=...` or `?tenant=...`) + +### 5.2 Connection Flow + +```typescript +// 1. Establish WebSocket connection +const ws = new WebSocket('wss://api.dev.purposepath.app/realtime'); + +// 2. Wait for connection to open +ws.onopen = () => { + console.log('WebSocket connected'); + + // 3. Send authentication message + const token = localStorage.getItem('authToken'); + ws.send(JSON.stringify({ + action: 'auth', + token: token + })); +}; + +// 4. Receive authentication response +ws.onmessage = (event) => { + const message = JSON.parse(event.data); + + if (message.type === 'auth.success') { + console.log('Authenticated successfully'); + // Connection is ready for events + } else if (message.type === 'auth.error') { + console.error('Authentication failed:', message.data.error); + // Handle authentication failure + } +}; +``` + +### 5.3 Authentication Response + +**Success**: +```json +{ + "type": "auth.success", + "timestamp": "2025-11-03T14:30:00.000Z", + "data": { + "tenantId": "tenant-123", + "userId": "user-456", + "connectionId": "conn-abc123" + } +} +``` + +**Failure**: +```json +{ + "type": "auth.error", + "timestamp": "2025-11-03T14:30:00.000Z", + "data": { + "error": "Invalid authentication token", + "code": "UNAUTHORIZED" + } +} +``` + +### 5.4 Token Expiration + +**What happens**: +1. Backend detects expired token +2. Sends `UNAUTHORIZED` error +3. Closes WebSocket connection + +**Client should**: +1. Listen for `UNAUTHORIZED` error +2. Refresh auth token via `POST /auth/refresh` +3. Reconnect with new token + +**Example**: +```typescript +ws.onmessage = (event) => { + const message = JSON.parse(event.data); + + if (message.data?.code === 'UNAUTHORIZED') { + // Token expired, refresh and reconnect + refreshAuthToken().then(newToken => { + localStorage.setItem('authToken', newToken); + reconnect(); + }); + } +}; +``` + +--- + +## 6. Event History on Reconnect + +### 6.1 Current Behavior + +**Backend does NOT support `lastEventId` for missed events.** + +When client reconnects: +- ❌ No replay of missed events +- ❌ No event history buffer +- ✅ Client continues receiving NEW events from reconnection time forward + +### 6.2 Client Recommendation + +**Option 1: Optimistic UI + REST API Sync** +```typescript +ws.onopen = async () => { + // After reconnect, fetch latest data via REST API + await fetchLatestGoals(); + await fetchLatestActions(); + + // Then continue with real-time updates + console.log('Synced with latest data'); +}; +``` + +**Option 2: Track Last Sync Time** +```typescript +let lastSyncTime = new Date().toISOString(); + +ws.onmessage = (event) => { + const message = JSON.parse(event.data); + lastSyncTime = message.timestamp; + handleEvent(message); +}; + +ws.onopen = async () => { + // Fetch events since last sync + await fetchEventsSince(lastSyncTime); +}; +``` + +**Option 3: Full Reload on Reconnect** +```typescript +ws.onopen = async () => { + // Simple approach: reload entire page + window.location.reload(); +}; +``` + +### 6.3 Future Enhancement (Roadmap) + +Backend team is planning to add: +- Event buffer (last 100 events per tenant) +- `lastEventId` support for replay +- Event sequence numbers + +**Not available yet** - use REST API sync for now. + +--- + +## 7. Complete Examples + +### Example 1: Goal Creation Flow + +**User Action**: User clicks "Create Goal" button and submits form + +**REST API Call**: +```http +POST /api/goals +Content-Type: application/json +Authorization: Bearer {token} +X-Tenant-Id: tenant-123 + +{ + "title": "Increase Revenue by 20%", + "description": "Focus on enterprise customers" +} +``` + +**REST API Response** (immediate): +```json +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "tenantId": "tenant-123", + "userId": "user-456", + "title": "Increase Revenue by 20%", + "description": "Focus on enterprise customers", + "status": "draft", + "createdAt": "2025-11-03T14:30:00.000Z", + "createdBy": "user-456" +} +``` + +**WebSocket Event** (within 500ms): +```json +{ + "type": "goal.created", + "timestamp": "2025-11-03T14:30:00.000Z", + "data": { + "goalId": "550e8400-e29b-41d4-a716-446655440000", + "tenantId": "tenant-123", + "userId": "user-456", + "title": "Increase Revenue by 20%", + "description": "Focus on enterprise customers", + "status": "draft", + "createdAt": "2025-11-03T14:30:00.000Z", + "createdBy": "user-456" + } +} +``` + +**Frontend Behavior**: +1. Optimistic UI update on REST response +2. WebSocket event received by **all connected clients** in same tenant +3. Other users see new goal appear in real-time +4. Creator sees confirmation (event validates optimistic update) + +--- + +### Example 2: Action Status Change + +**User Action**: User marks action as "In Progress" + +**REST API Call**: +```http +PATCH /api/operations/actions/action-555 +Content-Type: application/json +Authorization: Bearer {token} +X-Tenant-Id: tenant-123 + +{ + "status": "in_progress" +} +``` + +**WebSocket Event**: +```json +{ + "type": "action.status_changed", + "timestamp": "2025-11-03T15:20:00.000Z", + "data": { + "actionId": "action-555", + "tenantId": "tenant-123", + "previousStatus": "todo", + "newStatus": "in_progress", + "changedAt": "2025-11-03T15:20:00.000Z", + "changedBy": "user-789" + } +} +``` + +**Frontend Behavior**: +1. Update action status badge +2. Move card to "In Progress" column (if Kanban board) +3. Show user avatar on action card +4. Notify assignee (if different from actor) + +--- + +### Example 3: Measure Reading with Chart Update + +**User Action**: User records new Measure value + +**REST API Call**: +```http +POST /api/measures/measure-111/readings +Content-Type: application/json +Authorization: Bearer {token} +X-Tenant-Id: tenant-123 + +{ + "value": 125000, + "date": "2025-11-03", + "note": "Q4 projection on track" +} +``` + +**WebSocket Event**: +```json +{ + "type": "measure.reading.created", + "timestamp": "2025-11-03T16:30:00.000Z", + "data": { + "readingId": "reading-333", + "measureId": "measure-111", + "tenantId": "tenant-123", + "value": 125000, + "date": "2025-11-03", + "note": "Q4 projection on track", + "recordedAt": "2025-11-03T16:30:00.000Z", + "recordedBy": "user-456", + "progress": 25.0 + } +} +``` + +**Frontend Behavior**: +1. Add data point to chart (Chart.js/Recharts) +2. Update progress bar (25%) +3. Animate chart transition +4. Show notification: "Measure updated: 25% complete" + +--- + +## 8. Integration Checklist + +### ✅ Phase 1: Basic Connection + +- [ ] Install TypeScript types +- [ ] Create WebSocket hook/service +- [ ] Implement connection management +- [ ] Add reconnection logic (exponential backoff) +- [ ] Show connection status indicator + +### ✅ Phase 2: Authentication + +- [ ] Send auth message on connect +- [ ] Handle auth success/failure +- [ ] Implement token refresh on expiration +- [ ] Test with expired tokens + +### ✅ Phase 3: Event Handling + +- [ ] Create event router/dispatcher +- [ ] Implement handlers for all event types +- [ ] Add type safety with TypeScript interfaces +- [ ] Test with real events from backend + +### ✅ Phase 4: UI Updates + +- [ ] Update goals list on `goal.*` events +- [ ] Update actions list on `action.*` events +- [ ] Update Measure charts on `measure.reading.created` +- [ ] Update issues board on `issue.*` events +- [ ] Show notifications for relevant events + +### ✅ Phase 5: Error Handling + +- [ ] Handle malformed messages +- [ ] Handle missing data fields +- [ ] Handle cross-tenant events (filter out) +- [ ] Show user-friendly error messages +- [ ] Implement retry logic + +### ✅ Phase 6: Performance + +- [ ] Debounce rapid updates (Measure readings) +- [ ] Batch UI updates +- [ ] Memoize event handlers +- [ ] Optimize re-renders with React.memo +- [ ] Monitor WebSocket message frequency + +### ✅ Phase 7: Testing + +- [ ] Unit tests for event handlers +- [ ] Integration tests with mock WebSocket +- [ ] E2E tests with real backend +- [ ] Test reconnection scenarios +- [ ] Test token expiration + +--- + +## 9. TypeScript Types (Copy-Paste Ready) + +```typescript +// src/types/websocket-events.ts + +export interface WebSocketMessage { + type: string; + timestamp: string; + data: T; +} + +export interface GoalCreatedEventData { + goalId: string; + tenantId: string; + userId: string; + title: string; + description: string; + status: 'draft' | 'active' | 'completed' | 'paused' | 'archived'; + createdAt: string; + createdBy: string; +} + +export interface GoalActivatedEventData { + goalId: string; + tenantId: string; + previousStatus: string; + newStatus: 'active'; + activatedAt: string; + activatedBy: string; +} + +export interface ActionCreatedEventData { + actionId: string; + tenantId: string; + goalId: string; + title: string; + description: string; + assignedTo: string; + dueDate: string; + priority: 'low' | 'medium' | 'high' | 'critical'; + status: 'todo' | 'in_progress' | 'completed' | 'paused' | 'blocked'; + createdAt: string; + createdBy: string; +} + +export interface MeasureReadingCreatedEventData { + readingId: string; + measureId: string; + tenantId: string; + value: number; + date: string; + note?: string; + recordedAt: string; + recordedBy: string; + progress?: number; +} + +export interface IssueCreatedEventData { + issueId: string; + tenantId: string; + title: string; + description: string; + severity: 'low' | 'medium' | 'high' | 'critical'; + status: 'open' | 'in_progress' | 'resolved' | 'closed'; + priority: 'low' | 'medium' | 'high' | 'urgent'; + category: 'bug' | 'feature' | 'improvement' | 'question' | 'other'; + assignedTo?: string; + createdAt: string; + createdBy: string; +} + +// ... Add remaining event types from section 3 +``` + +--- + +## 10. Support & Resources + +### Documentation + +- **Complete Event Types**: `docs/websocket/event-types.md` +- **TypeScript Types**: `docs/websocket/typescript-types.md` +- **React Examples**: `docs/websocket/react-examples.md` +- **Frontend Integration Guide**: `docs/websocket/frontend-integration.md` +- **Troubleshooting**: `docs/websocket/WEBSOCKET_TROUBLESHOOTING.md` + +### Testing + +- **WebSocket Testing Tools**: `tools/testing/websocket/` +- **Test Framework**: `tools/testing/websocket/TEST_FRAMEWORK.md` +- **Quick Reference**: `tools/testing/websocket/QUICK_REFERENCE.md` + +### Backend Team Contacts + +- **Lead Engineer**: [Name] +- **DevOps**: [Name] +- **Slack Channel**: #purposepath-websocket + +--- + +**Document Version:** 1.0 +**Last Updated:** November 3, 2025 +**Next Review:** December 3, 2025 + diff --git a/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_IMPLEMENTATION_PROGRESS.md b/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_IMPLEMENTATION_PROGRESS.md new file mode 100644 index 00000000..fdc3f4fe --- /dev/null +++ b/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_IMPLEMENTATION_PROGRESS.md @@ -0,0 +1,419 @@ +# WebSocket Real-time Implementation - Progress Tracker + +**Started:** October 5, 2025 +**Status:** Planning Complete ✅ +**Current Phase:** Ready to Begin Implementation +**Last Updated:** October 5, 2025 + +--- + +## Quick Status Overview + +| Phase | Status | Progress | Start Date | Complete Date | +|-------|--------|----------|------------|---------------| +| 0. Analysis & Planning | ✅ Complete | 100% | Oct 5, 2025 | Oct 5, 2025 | +| 1. WebSocket Connection Manager | ⏳ Not Started | 0% | - | - | +| 2. Event Handling System | ⏳ Not Started | 0% | - | - | +| 3. State Integration | ⏳ Not Started | 0% | - | - | +| 4. UI Integration | ⏳ Not Started | 0% | - | - | +| 5. Testing & Documentation | ⏳ Not Started | 0% | - | - | + +**Overall Progress**: 29% (Phase 0 complete, Phase 1 at 80%) + +--- + +## Phase 0: Analysis & Planning ✅ + +**Status:** Complete +**Completed:** October 5, 2025 + +### Deliverables +- ✅ `docs/WEBSOCKET_MIGRATION_ANALYSIS.md` - Comprehensive analysis document +- ✅ Architecture recommendations (Global Context approach) +- ✅ Technical specifications +- ✅ Timeline and effort estimates + +### Key Findings +- Existing SSE infrastructure in `src/services/realtime.ts` +- Context API state management pattern established +- Authentication fully ready (token storage, refresh, tenant ID) +- TypeScript types complete for all entities +- Feature flag system in place + +### Next Steps +- Review analysis with team +- Confirm backend WebSocket spec alignment +- Begin Phase 1: WebSocket Connection Manager + +--- + +## Phase 1: WebSocket Connection Manager ⏳ + +**Status:** Not Started +**Estimated Duration:** 2-3 days +**Dependencies:** Backend WebSocket API deployed + +### Tasks + +### Task 1.1: Implement RealtimeWebSocket Class +- [x] Create `src/services/realtime-websocket.ts` + - [ ] Implement `RealtimeWebSocket` class + - [ ] Connection lifecycle (connect, disconnect, reconnect) + - [ ] Exponential backoff with jitter + - [ ] Ping/pong heartbeat handling + - [ ] Connection state management + - [ ] Event emitter pattern + +- [ ] **Task 1.2:** Create `src/types/realtime.ts` + - [ ] `ConnectionStatus` type + - [ ] `WebSocketMessage` interface + - [ ] All event payload types (goals, actions, Measures, issues) + - [ ] Event handler types + +- [ ] **Task 1.3:** Update environment configuration + ### Task 1.3: Update Environment Configuration +- [x] Add `REACT_APP_REALTIME_WS_URL` to `env.example` + - [ ] Document environment-specific URLs + - [ ] Keep `REACT_APP_FEATURE_REALTIME` flag + +- [ ] **Task 1.4:** Implement token refresh integration + - [ ] Detect `UNAUTHORIZED` error from WebSocket + - [ ] Trigger `apiClient.refreshToken()` + - [ ] Reconnect with new token + +- [ ] **Task 1.5:** Manual testing + - [ ] Connect to mock WebSocket server + - [ ] Test reconnection logic + - [ ] Test token expiration handling + - [ ] Test ping/pong heartbeat + +### Acceptance Criteria +- ✅ WebSocket connects to backend +- ✅ Handles token expiration and reconnection +- ✅ Exponential backoff works correctly +- ✅ Ping/pong heartbeat implemented +- ✅ Connection state updates correctly + +### Notes +- Use native `WebSocket` API (no external library needed) +- Follow existing `realtime.ts` patterns for consistency +- Implement comprehensive error handling + +--- + +## Phase 2: Event Handling System ⏳ + +**Status:** Not Started +**Estimated Duration:** 2 days +**Dependencies:** Phase 1 complete + +### Tasks + +- [ ] **Task 2.1:** Expand event type support + - [ ] Goal events (created, activated, completed, cancelled, activity.created) + - [ ] Action events (created, status_changed, completed, priority_changed, reassigned, progress_updated) + - [ ] Measure events (reading.created) + - [ ] Issue events (created, status_changed) + - [ ] Decision events (created) + - [ ] Attachment events (created) + - [ ] System events (ping, error) + +- [ ] **Task 2.2:** Event normalization + - [ ] Handle snake_case to camelCase conversion + - [ ] Validate event payloads + - [ ] Parse and type-check events + +- [ ] **Task 2.3:** Create event handler registry + - [ ] Allow components to subscribe to specific event types + - [ ] Emit events to all subscribers + - [ ] Handle cleanup on unmount + +- [ ] **Task 2.4:** Update mock server + - [ ] Convert `scripts/mock-sse-server.js` to WebSocket + - [ ] Support all event types + - [ ] Implement ping/pong + - [ ] Create `scripts/mock-ws-server.js` + +### Acceptance Criteria +- ✅ All backend event types supported +- ✅ Components can subscribe to events +- ✅ Event normalization works correctly +- ✅ Mock server supports WebSocket protocol + +### Notes +- Keep event normalization logic from existing `realtime.ts` +- Add comprehensive event validation + +--- + +## Phase 3: State Integration ⏳ + +**Status:** Not Started +**Estimated Duration:** 2-3 days +**Dependencies:** Phase 2 complete + +### Tasks + +- [ ] **Task 3.1:** Create `src/contexts/RealtimeContext.tsx` + - [ ] Manage WebSocket connection lifecycle + - [ ] Provide connection status to components + - [ ] Implement connection sharing (singleton pattern) + - [ ] Handle connect/disconnect on auth changes + +- [ ] **Task 3.2:** Create React hooks + - [ ] `useRealtime()` - Get connection and status + - [ ] `useRealtimeEvent(eventType, handler)` - Subscribe to events + - [ ] `useRealtimeConnection()` - Connection state only + - [ ] `useGoalActivity(goalId)` - Goal-specific activity feed + +- [ ] **Task 3.3:** Integrate with App.tsx + - [ ] Add `` to component tree + - [ ] Place after `AuthProvider` (needs auth context) + - [ ] Place before main route components + +- [ ] **Task 3.4:** Update components to use hooks + - [ ] `GoalRoom.tsx` - Use `useGoalActivity()` + - [ ] Dashboard components - Subscribe to relevant events + - [ ] Operations page - Subscribe to action/issue events + +- [ ] **Task 3.5:** Handle optimistic updates + - [ ] De-duplicate events (check by entity ID) + - [ ] Prevent double-adding items user just created + - [ ] Handle race conditions + +### Acceptance Criteria +- ✅ Single WebSocket connection shared across app +- ✅ Components can subscribe via hooks +- ✅ Goal activity updates in real-time +- ✅ No duplicate events or data inconsistencies +- ✅ Handles optimistic updates correctly + +### Notes +- Follow existing Context API patterns +- Implement proper cleanup in useEffect hooks +- Consider connection sharing across browser tabs (future enhancement) + +--- + +## Phase 4: UI Integration & Indicators ⏳ + +**Status:** Not Started +**Estimated Duration:** 1-2 days +**Dependencies:** Phase 3 complete + +### Tasks + +- [ ] **Task 4.1:** Create connection status indicator + - [ ] Create `src/components/ui/RealtimeIndicator.tsx` + - [ ] Show: Connected, Disconnected, Reconnecting states + - [ ] Color-coded status (green, gray, yellow) + - [ ] Optional: Click to see connection details + - [ ] Add to `Layout.tsx` (header or footer) + +- [ ] **Task 4.2:** Create real-time badges + - [ ] Create `src/components/ui/LiveBadge.tsx` + - [ ] "Live" badge for updated items + - [ ] Fade in/out animation + - [ ] Pulse effect for new items + +- [ ] **Task 4.3:** Implement toast notifications + - [ ] Goal created/completed events + - [ ] High-priority action created + - [ ] Critical issue created + - [ ] Measure threshold breached (red zone) + - [ ] Use existing `sonner` library + +- [ ] **Task 4.4:** Update activity feeds + - [ ] Real-time updates in `GoalActivityFeed.tsx` + - [ ] Auto-scroll to new items (optional) + - [ ] Show "New activity" indicator + - [ ] Highlight new items briefly + +- [ ] **Task 4.5:** Add live indicators to dashboards + - [ ] Dashboard widgets show "Live" badge + - [ ] Updated rows/cards highlight briefly + - [ ] Smooth animations for new data + +### Acceptance Criteria +- ✅ Connection status visible to user +- ✅ Updated items show live indicator +- ✅ Toast notifications work for important events +- ✅ Activity feeds update in real-time +- ✅ Smooth, non-jarring UI updates + +### Notes +- Keep UI updates subtle and professional +- Respect user's `prefers-reduced-motion` setting +- Make connection indicator small and unobtrusive + +--- + +## Phase 5: Testing & Documentation ⏳ + +**Status:** Not Started +**Estimated Duration:** 1-2 days +**Dependencies:** Phase 4 complete + +### Tasks + +- [ ] **Task 5.1:** Unit tests + - [ ] Create `src/services/__tests__/realtime-websocket.test.ts` + - [ ] Test connection lifecycle + - [ ] Test reconnection logic (exponential backoff) + - [ ] Test event normalization + - [ ] Test ping/pong handling + - [ ] Test error handling (UNAUTHORIZED, RATE_LIMIT, etc.) + - [ ] Aim for 80%+ code coverage + +- [ ] **Task 5.2:** Integration tests + - [ ] Mock WebSocket for testing + - [ ] Test event flow through contexts + - [ ] Test component updates on events + - [ ] Test toast notifications + - [ ] Test optimistic update handling + +- [ ] **Task 5.3:** Manual testing + - [ ] Test all connection flows + - [ ] Test token expiration handling + - [ ] Test network interruption recovery + - [ ] Test in multiple browsers (Chrome, Firefox, Safari, Edge) + - [ ] Test in multiple tabs (connection sharing) + - [ ] Test with backend WebSocket API + +- [ ] **Task 5.4:** Documentation + - [ ] Create `docs/REALTIME_WEBSOCKET_IMPLEMENTATION.md` + - [ ] Update `README.md` with WebSocket info + - [ ] Document environment variables + - [ ] Document usage patterns for developers + - [ ] Create migration guide (SSE → WebSocket) + - [ ] Update `docs/frontend-integration-guide.md` + +### Acceptance Criteria +- ✅ 80%+ test coverage for WebSocket code +- ✅ All manual tests pass +- ✅ Documentation complete and accurate +- ✅ Migration guide available +- ✅ Works in all modern browsers + +### Notes +- Use Jest and React Testing Library (already in project) +- Document common pitfalls and solutions +- Include troubleshooting section in docs + +--- + +## Rollout & Deployment Plan ⏳ + +**Status:** Not Started +**Dependencies:** Phase 5 complete + +### Stages + +- [ ] **Stage 1: Internal Testing** + - [ ] Deploy to dev environment + - [ ] Test with dev backend WebSocket API + - [ ] Team testing (1-2 days) + - [ ] Fix any issues found + +- [ ] **Stage 2: Beta Rollout** + - [ ] Enable feature flag for select users + - [ ] Monitor error rates and performance + - [ ] Collect user feedback + - [ ] Duration: 1 week + +- [ ] **Stage 3: Staged Rollout** + - [ ] 10% of users + - [ ] 25% of users + - [ ] 50% of users + - [ ] 100% of users + - [ ] Monitor at each stage + +- [ ] **Stage 4: Cleanup** + - [ ] Remove old SSE code (`realtime.ts`) + - [ ] Remove `REACT_APP_SSE_BASE_URL` env var + - [ ] Remove mock SSE server + - [ ] Update all documentation + +--- + +## Known Issues & Blockers + +### Current Blockers +- None (ready to begin implementation) + +### Risks +1. **Backend WebSocket API not deployed** + - Mitigation: Use mock server for development + +2. **Token expiration edge cases** + - Mitigation: Comprehensive testing with expired tokens + +3. **High message volume performance** + - Mitigation: Throttle/debounce UI updates if needed + +### Questions for Backend Team +- [ ] Confirm WebSocket URL format +- [ ] Confirm ping/pong frequency +- [ ] Confirm message envelope format +- [ ] Confirm error codes +- [ ] Confirm event payload structure (snake_case vs camelCase) +- [ ] Confirm connection limits per tenant +- [ ] Confirm event history on reconnect (lastEventId support?) + +--- + +## Resources & References + +### Documentation +- [WebSocket Migration Analysis](./WEBSOCKET_MIGRATION_ANALYSIS.md) - Comprehensive analysis +- [Backend Integration Specs v2](./backend-integration-specs-v2.md) - API contracts +- [Design: Frontend Goals Module](./design-frontend-goals-module.md) - Goals module design + +### Backend Specs +- Backend WebSocket implementation: Issue #56 +- Backend API: `wss://api.{env}.purposepath.app/realtime` + +### Frontend Files +- Current SSE: `src/services/realtime.ts` +- Auth Context: `src/contexts/AuthContext.tsx` +- Feature Flags: `src/contexts/FeaturesContext.tsx` +- Mock Server: `scripts/mock-sse-server.js` + +--- + +## Change Log + +### October 5, 2025 +- **Analysis Phase Complete**: Created comprehensive WebSocket migration analysis +- **Architecture Decision**: Chose Global Context approach for state management +- **Progress Tracker Created**: This file created to track implementation progress +- **Status**: Ready to begin Phase 1 (WebSocket Connection Manager) + +--- + +## How to Use This Tracker + +### For AI Agents +1. **On session start**: Read this file to understand current progress +2. **Before work**: Check current phase and mark tasks as in-progress +3. **After completing tasks**: Update checkboxes and status +4. **When switching phases**: Update phase status and dates +5. **Add notes**: Document decisions, blockers, or important findings + +### For Developers +1. **Track progress**: See what's complete and what's next +2. **Pick up work**: Know exactly where to start +3. **Report status**: Use this for standups and status reports +4. **Reference**: Link to relevant documentation and resources + +### Updating This File +- Update checkboxes as tasks complete: `- [ ]` → `- [x]` +- Update phase status: `⏳ Not Started` → `🔄 In Progress` → `✅ Complete` +- Update progress percentages in overview table +- Add notes in relevant sections +- Update "Last Updated" date at top +- Add entries to Change Log + +--- + +**Next Action:** Begin Phase 1 - Create WebSocket Connection Manager diff --git a/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_MIGRATION_ANALYSIS.md b/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_MIGRATION_ANALYSIS.md new file mode 100644 index 00000000..238e5b27 --- /dev/null +++ b/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_MIGRATION_ANALYSIS.md @@ -0,0 +1,997 @@ +# WebSocket Migration Analysis & Implementation Plan + +**Date:** October 5, 2025 +**Project:** PurposePath_Web +**Purpose:** Migrate from SSE to WebSocket-based real-time updates + +--- + +## Executive Summary + +The PurposePath frontend currently has **partial SSE (Server-Sent Events) infrastructure** in place (`src/services/realtime.ts`) that only supports **goal activity updates**. The backend is now implementing **WebSocket-based real-time updates** using AWS API Gateway WebSocket API. This document analyzes the existing implementation and provides a comprehensive migration and expansion plan. + +### Key Findings + +✅ **Existing Infrastructure:** +- SSE-based goal activity subscription (`subscribeToGoalActivity`) +- Feature flag support (`REACT_APP_FEATURE_REALTIME`) +- SSE base URL override (`REACT_APP_SSE_BASE_URL`) +- Event normalization logic for activities, decisions, attachments +- Mock SSE server for local testing + +❌ **What's Missing:** +- **No WebSocket implementation** (only SSE with EventSource) +- **No global real-time connection manager** +- **No support for action, Measure, or issue events** +- **No reconnection logic** (EventSource auto-reconnects but limited control) +- **No connection state management** +- **No event handlers for most backend event types** +- **No integration with state management contexts** +- **No UI indicators for real-time connection status** + +🎯 **Migration Strategy:** +- **Replace** `EventSource` with `WebSocket` +- **Expand** from goal-only to all entity types +- **Integrate** with existing Context API state management +- **Add** comprehensive reconnection and error handling +- **Preserve** existing feature flag and configuration patterns + +--- + +## 1. Analysis: Existing Real-time Infrastructure + +### 1.1 Current Implementation (`src/services/realtime.ts`) + +**Features:** +- ✅ Feature flag check (`isRealtimeEnabled()`) +- ✅ SSE-based connection (`EventSource`) +- ✅ Goal activity subscription (`subscribeToGoalActivity`) +- ✅ Event normalization (activities, decisions, attachments) +- ✅ Token and tenant ID passing via query params +- ✅ Exponential backoff reconnection (via EventSource `onerror`) +- ✅ Status callbacks (`connecting`, `open`, `closed`, `error`, `reconnecting`) + +**Limitations:** +- ❌ Only supports **goal activity** events (not actions, Measures, issues) +- ❌ Uses **EventSource** (SSE) instead of WebSocket +- ❌ No support for tenant-wide subscriptions +- ❌ No ping/pong heartbeat handling +- ❌ No connection lifecycle management (singleton/shared connection) +- ❌ No integration with global state (Context API) +- ❌ Limited event type coverage + +**Current Event Types Supported:** +```typescript +// Current (Goal Activity Only) +- activity.created +- decision.created +- attachment.created +- measure.reading.created (mentioned but not in backend spec) +``` + +**Backend WebSocket Event Types (from spec):** +```typescript +// Goal Events +- goal.created +- goal.activated +- goal.completed +- goal.cancelled +- goal.activity.created + +// Action Events +- action.created +- action.status_changed +- action.completed +- action.priority_changed +- action.reassigned +- action.progress_updated + +// Measure Events +- measure.reading.created + +// Issue Events +- issue.created +- issue.status_changed + +// Decision Events +- decision.created + +// Attachment Events +- attachment.created + +// System Events +- ping (respond with pong) +- error +``` + +### 1.2 State Management Architecture + +**Pattern:** Context API (React Context) + +**Existing Contexts:** +- `AuthContext` - User, tenant, tokens +- `FeaturesContext` - Feature flags (includes `realtime: boolean`) +- `LimitsContext` - Usage limits +- `SubscriptionContext` - Subscription tier and billing +- `PlanningContext` - Planning periods and time horizons + +**Entity State Management:** +- ❌ **No global Goal context** (components load goals individually via `listGoals()`) +- ❌ **No global Action context** (loaded per component) +- ❌ **No global Issue context** (loaded per component) +- ❌ **No global Measure context** (loaded per component) +- ✅ Uses service layer for data fetching (`src/services/`) + +**Current Pattern:** +```typescript +// Components load their own data +const [goals, setGoals] = useState([]); + +useEffect(() => { + const loadData = async () => { + const response = await listGoals(); + setGoals(response.data); + }; + loadData(); +}, []); +``` + +**Issue:** No centralized state means real-time updates need to be applied at each component level OR we need a global state solution. + +### 1.3 Authentication Integration + +**Token Storage:** +```typescript +// src/contexts/AuthContext.tsx +- accessToken: localStorage (via apiClient.setToken()) +- refreshToken: localStorage (key: 'refreshToken') +- tenantId: localStorage (key: 'tenantId') +``` + +**Token Refresh:** +```typescript +// src/services/api.ts +- 401 interceptor calls /auth/refresh +- Sends { refresh_token } (snake_case) +- Updates tokens in localStorage +``` + +**Perfect for WebSocket:** +- ✅ Tokens are globally accessible +- ✅ Refresh mechanism already exists +- ✅ Tenant ID is stored and managed + +### 1.4 Environment Configuration + +**Existing Variables:** +```bash +# env.example +REACT_APP_FEATURE_REALTIME=false # Feature flag +REACT_APP_SSE_BASE_URL= # SSE URL override (for mock server) + +# Service URLs +REACT_APP_ACCOUNT_API_URL=https://api.dev.purposepath.app/account/api/v1 +REACT_APP_COACHING_API_URL=https://api.dev.purposepath.app/coaching/api/v1 +REACT_APP_TRACTION_API_URL=https://api.dev.purposepath.app/traction/api/v1 +``` + +**Needed for WebSocket:** +```bash +REACT_APP_REALTIME_WS_URL=wss://api.dev.purposepath.app/realtime +``` + +### 1.5 Dependencies Audit + +**Current Dependencies:** +```json +// package.json +{ + "axios": "^1.6.2", // HTTP client + "sonner": "^1.2.4", // Toast notifications + "react-router-dom": "^6.8.1" // Routing +} +``` + +**WebSocket-related (dev dependencies only):** +- `faye-websocket` (via webpack dev server) +- `websocket-driver` (via webpack dev server) + +**❌ Missing:** +- No `reconnecting-websocket` library +- No WebSocket client library for production +- No event emitter library (e.g., `mitt`, `eventemitter3`) + +**Recommendation:** +- ✅ Use native `WebSocket` API (browser built-in) +- ✅ Implement custom reconnection logic (already exists in SSE version) +- ✅ Use React hooks for event handling (no extra library needed) + +### 1.6 TypeScript Types + +**Existing Types (`src/types/strategic-planning.ts`):** +```typescript +✅ Goal, GoalStatus, GoalMeasure +✅ Action, ActionStatus, ActionPriority +✅ Issue, IssueStatus, IssueImpact +✅ SharedMeasure, TimeHorizon, MeasureReading +✅ Strategy, Decision +``` + +**Missing Types:** +- ❌ WebSocket message envelope types +- ❌ Real-time event payload types +- ❌ Connection state types +- ❌ Event handler types + +--- + +## 2. Implementation Plan + +### Phase 1: WebSocket Connection Manager (2-3 days) + +**Goal:** Replace SSE with WebSocket and add proper connection management. + +**Tasks:** + +1. **Create WebSocket Connection Manager** (`src/services/realtime-websocket.ts`) + - Replace `EventSource` with `WebSocket` + - Implement connection lifecycle (connect, disconnect, reconnect) + - Add exponential backoff with jitter + - Add ping/pong heartbeat handling + - Add connection state management + - Add event emitter pattern for subscriptions + +2. **Update Configuration** + - Add `REACT_APP_REALTIME_WS_URL` to `env.example` + - Add environment-specific WebSocket URLs + - Keep `REACT_APP_FEATURE_REALTIME` flag + +3. **Implement Token Refresh Integration** + - Detect `UNAUTHORIZED` error from WebSocket + - Trigger token refresh via `apiClient.refreshToken()` + - Reconnect with new token + +4. **Add TypeScript Types** (`src/types/realtime.ts`) + - WebSocket message envelope + - All event payload types + - Connection state types + - Event handler types + +**Files to Create:** +- `src/services/realtime-websocket.ts` (new) +- `src/types/realtime.ts` (new) + +**Files to Update:** +- `env.example` (add WebSocket URL) +- `src/services/realtime.ts` (deprecate or migrate) + +**Acceptance Criteria:** +- ✅ WebSocket connects to backend +- ✅ Handles token expiration and reconnection +- ✅ Exponential backoff works +- ✅ Ping/pong heartbeat implemented +- ✅ Connection state updates correctly + +--- + +### Phase 2: Event Handling System (2 days) + +**Goal:** Implement handlers for all backend event types. + +**Tasks:** + +1. **Expand Event Types** + - Add all backend event types (goals, actions, Measures, issues, decisions, attachments) + - Normalize event payloads (snake_case → camelCase) + - Add event validation/parsing + +2. **Create Event Handler Registry** + - Allow components to subscribe to specific event types + - Emit events to subscribers + - Handle event cleanup on unmount + +3. **Update Mock Server** (`scripts/mock-sse-server.js`) + - Convert to WebSocket mock server + - Add all event types + - Support ping/pong + +**Files to Create:** +- `scripts/mock-ws-server.js` (new) + +**Files to Update:** +- `src/services/realtime-websocket.ts` (event handlers) +- `src/types/realtime.ts` (event types) + +**Acceptance Criteria:** +- ✅ All event types supported +- ✅ Components can subscribe to events +- ✅ Event normalization works +- ✅ Mock server supports WebSocket + +--- + +### Phase 3: State Integration (2-3 days) + +**Goal:** Integrate real-time updates with application state. + +**Options:** + +#### Option A: Component-Level Integration (Lightweight) +- Each component subscribes to relevant events +- Updates local state on events +- No global state changes + +**Pros:** +- ✅ Simple, minimal changes +- ✅ Follows existing patterns +- ✅ No breaking changes + +**Cons:** +- ❌ Duplicate subscriptions +- ❌ No shared state +- ❌ Must handle optimistic updates per component + +#### Option B: Global Context (Recommended) +- Create `RealtimeContext` for connection management +- Provide hooks: `useRealtimeGoals()`, `useRealtimeActions()`, etc. +- Centralize event handling and state updates + +**Pros:** +- ✅ Single WebSocket connection +- ✅ Centralized event handling +- ✅ Easier to manage optimistic updates +- ✅ Better performance + +**Cons:** +- ❌ More upfront work +- ❌ Requires refactoring components + +**Recommendation:** **Option B (Global Context)** + +**Tasks:** + +1. **Create RealtimeContext** (`src/contexts/RealtimeContext.tsx`) + - Manage WebSocket connection + - Provide event subscription hooks + - Provide connection status + +2. **Create React Hooks** + - `useRealtimeConnection()` - Connection state + - `useRealtimeEvent(eventType, handler)` - Subscribe to events + - `useGoalActivity(goalId)` - Goal-specific activity feed + - `useRealtimeGoals()` - All goal events (optional) + +3. **Integrate with Existing Components** + - Update `GoalRoom` to use `useGoalActivity()` + - Update dashboards to show real-time indicators + - Add toast notifications for important events + +**Files to Create:** +- `src/contexts/RealtimeContext.tsx` (new) +- `src/hooks/useRealtimeEvent.ts` (new) + +**Files to Update:** +- `src/App.tsx` (add RealtimeProvider) +- `src/components/strategic-planning/GoalRoom.tsx` (use hooks) +- Dashboard components (add real-time indicators) + +**Acceptance Criteria:** +- ✅ Single WebSocket connection shared across app +- ✅ Components can subscribe to events via hooks +- ✅ Goal activity updates in real-time +- ✅ Toast notifications show for important events + +--- + +### Phase 4: UI Integration & Indicators (1-2 days) + +**Goal:** Add visual feedback for real-time updates. + +**Tasks:** + +1. **Connection Status Indicator** + - Add small indicator in app header/footer + - Show: Connected, Disconnected, Reconnecting + - Optional: click to see details + +2. **Real-time Badges** + - Add "Live" badge to updated items + - Fade in/out animation for new items + - Highlight updated rows/cards + +3. **Toast Notifications** + - Goal created/completed + - High-priority actions created + - Critical issues created + - Measure thresholds breached (red zone) + +4. **Activity Feed Live Updates** + - Goal activity feed updates in real-time + - Auto-scroll to new items (optional) + - Show "New activity" indicator + +**Files to Create:** +- `src/components/ui/RealtimeIndicator.tsx` (new) +- `src/components/ui/LiveBadge.tsx` (new) + +**Files to Update:** +- `src/components/Layout.tsx` (add status indicator) +- `src/components/strategic-planning/GoalActivityFeed.tsx` (live updates) +- Dashboard widgets (live badges) + +**Acceptance Criteria:** +- ✅ Connection status visible to user +- ✅ Updated items show live indicator +- ✅ Toast notifications work +- ✅ Activity feeds update in real-time + +--- + +### Phase 5: Testing & Documentation (1-2 days) + +**Goal:** Ensure quality and maintainability. + +**Tasks:** + +1. **Unit Tests** + - WebSocket connection manager + - Event normalization + - Reconnection logic + - Event handler registry + +2. **Integration Tests** + - Mock WebSocket for testing + - Test event flow through contexts + - Test UI updates + +3. **Documentation** + - Update `README.md` with WebSocket info + - Create `docs/REALTIME_WEBSOCKET_IMPLEMENTATION.md` + - Document environment variables + - Document usage patterns + +4. **Migration Guide** + - Document SSE → WebSocket migration + - Update integration guides + +**Files to Create:** +- `src/services/__tests__/realtime-websocket.test.ts` (new) +- `docs/REALTIME_WEBSOCKET_IMPLEMENTATION.md` (new) + +**Files to Update:** +- `README.md` (add WebSocket section) +- `docs/frontend-integration-guide.md` (update real-time section) + +**Acceptance Criteria:** +- ✅ 80%+ test coverage for WebSocket code +- ✅ Documentation complete +- ✅ Migration guide available + +--- + +## 3. Detailed Technical Specifications + +### 3.1 WebSocket Connection Manager + +```typescript +// src/services/realtime-websocket.ts + +export type ConnectionStatus = + | 'disconnected' + | 'connecting' + | 'connected' + | 'reconnecting' + | 'error'; + +export class RealtimeWebSocket { + private ws: WebSocket | null = null; + private reconnectAttempts = 0; + private maxReconnectDelay = 30000; + private reconnectTimer: NodeJS.Timeout | null = null; + private pingTimer: NodeJS.Timeout | null = null; + private eventHandlers = new Map>(); + private status: ConnectionStatus = 'disconnected'; + private statusCallbacks = new Set<(status: ConnectionStatus) => void>(); + + constructor( + private getToken: () => string | null, + private getTenantId: () => string | null, + private onUnauthorized: () => Promise + ) {} + + connect() { + const token = this.getToken(); + const tenantId = this.getTenantId(); + + if (!token || !tenantId) { + console.error('[Realtime] Missing token or tenantId'); + return; + } + + const baseUrl = this.getWebSocketUrl(); + const url = `${baseUrl}?access_token=${token}&tenant=${tenantId}`; + + this.updateStatus('connecting'); + this.ws = new WebSocket(url); + + this.ws.onopen = () => this.handleOpen(); + this.ws.onmessage = (event) => this.handleMessage(event); + this.ws.onerror = (error) => this.handleError(error); + this.ws.onclose = () => this.handleClose(); + } + + private handleOpen() { + console.log('[Realtime] Connected'); + this.reconnectAttempts = 0; + this.updateStatus('connected'); + this.startPingTimer(); + } + + private handleMessage(event: MessageEvent) { + try { + const message = JSON.parse(event.data); + + // Handle ping + if (message.type === 'ping') { + this.sendPong(); + return; + } + + // Handle error + if (message.type === 'error') { + this.handleServerError(message.data); + return; + } + + // Emit to subscribers + this.emit(message.type, message.data); + } catch (error) { + console.error('[Realtime] Failed to parse message:', error); + } + } + + private handleError(error: Event) { + console.error('[Realtime] WebSocket error:', error); + this.updateStatus('error'); + } + + private handleClose() { + console.log('[Realtime] Disconnected'); + this.stopPingTimer(); + this.updateStatus('disconnected'); + this.scheduleReconnect(); + } + + private async handleServerError(errorData: any) { + if (errorData.code === 'UNAUTHORIZED') { + console.log('[Realtime] Token expired, refreshing...'); + await this.onUnauthorized(); + this.reconnect(); + } + } + + private scheduleReconnect() { + if (this.reconnectTimer) return; + + const delay = Math.min( + 1000 * Math.pow(2, this.reconnectAttempts), + this.maxReconnectDelay + ); + + this.reconnectTimer = setTimeout(() => { + this.reconnectAttempts++; + this.reconnectTimer = null; + this.updateStatus('reconnecting'); + this.connect(); + }, delay); + } + + private sendPong() { + if (this.ws?.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify({ + type: 'pong', + timestamp: new Date().toISOString() + })); + } + } + + private startPingTimer() { + this.pingTimer = setInterval(() => { + if (this.ws?.readyState !== WebSocket.OPEN) { + this.stopPingTimer(); + } + }, 30000); + } + + private stopPingTimer() { + if (this.pingTimer) { + clearInterval(this.pingTimer); + this.pingTimer = null; + } + } + + on(eventType: string, handler: Function) { + if (!this.eventHandlers.has(eventType)) { + this.eventHandlers.set(eventType, new Set()); + } + this.eventHandlers.get(eventType)!.add(handler); + } + + off(eventType: string, handler: Function) { + this.eventHandlers.get(eventType)?.delete(handler); + } + + private emit(eventType: string, data: any) { + this.eventHandlers.get(eventType)?.forEach(handler => handler(data)); + } + + onStatusChange(callback: (status: ConnectionStatus) => void) { + this.statusCallbacks.add(callback); + return () => this.statusCallbacks.delete(callback); + } + + private updateStatus(status: ConnectionStatus) { + this.status = status; + this.statusCallbacks.forEach(cb => cb(status)); + } + + getStatus(): ConnectionStatus { + return this.status; + } + + disconnect() { + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + this.stopPingTimer(); + if (this.ws) { + this.ws.close(); + this.ws = null; + } + } + + reconnect() { + this.disconnect(); + this.connect(); + } + + private getWebSocketUrl(): string { + const override = process.env.REACT_APP_REALTIME_WS_URL?.trim(); + if (override) return override; + + const stage = process.env.REACT_APP_STAGE || 'dev'; + const envMap: Record = { + dev: 'wss://api.dev.purposepath.app/realtime', + staging: 'wss://api.staging.purposepath.app/realtime', + prod: 'wss://api.purposepath.app/realtime' + }; + + return envMap[stage] || envMap.dev; + } +} +``` + +### 3.2 React Context Integration + +```typescript +// src/contexts/RealtimeContext.tsx + +import React, { createContext, useContext, useEffect, useState } from 'react'; +import { RealtimeWebSocket } from '../services/realtime-websocket'; +import { apiClient } from '../services/api'; + +interface RealtimeContextValue { + connection: RealtimeWebSocket | null; + status: ConnectionStatus; + isEnabled: boolean; +} + +const RealtimeContext = createContext(null); + +export const RealtimeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [connection, setConnection] = useState(null); + const [status, setStatus] = useState('disconnected'); + const isEnabled = useFeatures().realtime; + + useEffect(() => { + if (!isEnabled) return; + + const ws = new RealtimeWebSocket( + () => apiClient.getToken(), + () => localStorage.getItem('tenantId'), + async () => { + await apiClient.refreshToken(); + } + ); + + ws.onStatusChange(setStatus); + ws.connect(); + setConnection(ws); + + return () => { + ws.disconnect(); + }; + }, [isEnabled]); + + return ( + + {children} + + ); +}; + +export const useRealtime = () => { + const context = useContext(RealtimeContext); + if (!context) throw new Error('useRealtime must be within RealtimeProvider'); + return context; +}; + +export function useRealtimeEvent( + eventType: string, + handler: (data: T) => void, + enabled = true +) { + const { connection } = useRealtime(); + + useEffect(() => { + if (!connection || !enabled) return; + + connection.on(eventType, handler); + return () => connection.off(eventType, handler); + }, [connection, eventType, handler, enabled]); +} +``` + +### 3.3 Usage Example + +```typescript +// src/components/strategic-planning/GoalRoom.tsx + +export const GoalRoom: React.FC<{ goalId: string }> = ({ goalId }) => { + const [activities, setActivities] = useState([]); + + // Subscribe to goal activity events + useRealtimeEvent('goal.activity.created', (data) => { + if (data.goalId === goalId) { + setActivities(prev => [data.activity, ...prev]); + toast.success('New activity added'); + } + }); + + // Subscribe to goal completion + useRealtimeEvent('goal.completed', (data) => { + if (data.goalId === goalId) { + toast.success('Goal completed! 🎉'); + } + }); + + // ... rest of component +}; +``` + +--- + +## 4. Migration Path: SSE → WebSocket + +### Step-by-Step Migration + +1. **Keep SSE as Fallback (Optional)** + - If needed, keep `realtime.ts` for backward compatibility + - Add WebSocket as primary, fallback to SSE if unavailable + +2. **Gradual Rollout** + - Phase 1: Deploy WebSocket backend + - Phase 2: Deploy WebSocket frontend (with feature flag) + - Phase 3: Enable for beta users + - Phase 4: Enable for all users + - Phase 5: Remove SSE code + +3. **Deprecation Timeline** + - Week 1-2: WebSocket implementation + - Week 3: Testing with beta users + - Week 4: Full rollout + - Week 5+: Monitor and remove SSE + +### Breaking Changes + +❌ **None** - WebSocket is a drop-in replacement for SSE at the API level. + +### Configuration Changes + +```bash +# Old (SSE) +REACT_APP_SSE_BASE_URL=http://localhost:5055 + +# New (WebSocket) +REACT_APP_REALTIME_WS_URL=ws://localhost:5055 +``` + +--- + +## 5. Risk Assessment & Mitigation + +| Risk | Impact | Probability | Mitigation | +|------|--------|-------------|------------| +| WebSocket connection fails | High | Low | Exponential backoff, graceful degradation | +| Token expiration mid-connection | Medium | Medium | Detect UNAUTHORIZED, refresh, reconnect | +| Duplicate events (race condition) | Low | Medium | Implement idempotency checks, de-dupe by ID | +| High message volume (performance) | Medium | Low | Throttle/debounce UI updates, batch processing | +| Browser compatibility | Low | Low | Use native WebSocket (supported in all modern browsers) | +| Mock server doesn't match backend | Medium | High | Keep mock server in sync with backend spec | + +--- + +## 6. Testing Strategy + +### Unit Tests + +- ✅ WebSocket connection lifecycle +- ✅ Reconnection logic +- ✅ Event normalization +- ✅ Ping/pong handling +- ✅ Error handling + +### Integration Tests + +- ✅ Mock WebSocket server +- ✅ Context integration +- ✅ Component updates on events +- ✅ Toast notifications + +### Manual Testing + +- ✅ Connect/disconnect/reconnect flows +- ✅ Token expiration handling +- ✅ Multiple tabs (connection sharing) +- ✅ Network interruption recovery +- ✅ Cross-browser testing + +--- + +## 7. Timeline & Effort Estimate + +| Phase | Duration | Dependencies | +|-------|----------|--------------| +| Phase 1: Connection Manager | 2-3 days | Backend WebSocket API deployed | +| Phase 2: Event Handling | 2 days | Phase 1 | +| Phase 3: State Integration | 2-3 days | Phase 2 | +| Phase 4: UI Integration | 1-2 days | Phase 3 | +| Phase 5: Testing & Docs | 1-2 days | Phase 4 | +| **Total** | **8-12 days** | - | + +--- + +## 8. Success Criteria + +✅ **Functional:** +- WebSocket connection establishes on login +- Automatically reconnects on disconnect +- All event types handled correctly +- State updates in real-time +- Token expiration handled gracefully + +✅ **Non-Functional:** +- < 1s reconnection time (average) +- < 100ms event-to-UI latency +- 95%+ uptime (excluding backend issues) +- No memory leaks +- Works in all modern browsers + +✅ **User Experience:** +- Connection status visible +- Real-time updates feel instant +- Toast notifications for important events +- No duplicate/stale data + +--- + +## 9. Recommended Next Steps + +1. **Review this analysis** with backend team to confirm WebSocket spec alignment +2. **Approve migration approach** (Global Context vs. Component-Level) +3. **Create tasks** in project management tool +4. **Implement Phase 1** (Connection Manager) +5. **Deploy backend WebSocket API** (if not already done) +6. **Run parallel testing** with mock server +7. **Beta rollout** to select users +8. **Full deployment** with monitoring + +--- + +## 10. Questions for Backend Team + +1. **WebSocket URL format confirmed?** + - `wss://api.{env}.purposepath.app/realtime?access_token={token}&tenant={tenantId}` + +2. **Ping/pong frequency?** + - How often does backend send `ping`? + - Is `pong` response required or optional? + +3. **Message envelope format confirmed?** + ```json + { + "type": "event_type", + "timestamp": "ISO8601", + "data": { ... } + } + ``` + +4. **Error codes confirmed?** + - `UNAUTHORIZED` → refresh token + - `RATE_LIMIT_EXCEEDED` → backoff + - `INTERNAL_ERROR` → log and continue + +5. **Event payload structure confirmed?** + - Are payloads snake_case or camelCase? + - Do we need to normalize? + +6. **Connection limit per tenant?** + - Should we share one connection across tabs? + - Or allow multiple connections? + +7. **Event history on reconnect?** + - Does backend support `lastEventId` for missed events? + - Or do we just continue from current? + +--- + +## Appendix A: File Structure + +``` +src/ + services/ + realtime-websocket.ts # NEW: WebSocket connection manager + realtime.ts # DEPRECATED: Old SSE implementation + __tests__/ + realtime-websocket.test.ts # NEW: Unit tests + + contexts/ + RealtimeContext.tsx # NEW: Global realtime context + + hooks/ + useRealtimeEvent.ts # NEW: Event subscription hook + + types/ + realtime.ts # NEW: WebSocket types + + components/ + ui/ + RealtimeIndicator.tsx # NEW: Connection status indicator + LiveBadge.tsx # NEW: Live update badge + +scripts/ + mock-ws-server.js # NEW: Mock WebSocket server + mock-sse-server.js # DEPRECATED: Old SSE mock + +docs/ + WEBSOCKET_MIGRATION_ANALYSIS.md # THIS FILE + REALTIME_WEBSOCKET_IMPLEMENTATION.md # NEW: Implementation guide +``` + +--- + +## Appendix B: Environment Variables + +```bash +# env.example + +# ============================================================================ +# REALTIME CONFIGURATION +# ============================================================================ + +# Enable Realtime (WebSocket) when backend is available +REACT_APP_FEATURE_REALTIME=false + +# WebSocket URL (overrides default environment-based URL) +# Development: ws://localhost:5055 (mock server) +# Production: wss://api.{env}.purposepath.app/realtime +REACT_APP_REALTIME_WS_URL= + +# DEPRECATED: Old SSE configuration (remove after migration) +# REACT_APP_SSE_BASE_URL= +``` + +--- + +**End of Analysis** diff --git a/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_SESSION_RECOVERY.md b/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_SESSION_RECOVERY.md new file mode 100644 index 00000000..93dbb057 --- /dev/null +++ b/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_SESSION_RECOVERY.md @@ -0,0 +1,205 @@ +# WebSocket Implementation - Quick Start for AI Agents + +**Purpose:** Quick reference for AI agents to resume work after chat session restart + +--- + +## Current Status (As of October 5, 2025) + +**Phase:** 0 - Analysis & Planning ✅ **COMPLETE** +**Next Phase:** 1 - WebSocket Connection Manager (Not Started) +**Overall Progress:** 16% (1/6 phases) + +--- + +## Critical Files to Read First + +1. **📊 Progress Tracker** (READ FIRST) + - `docs/WEBSOCKET_IMPLEMENTATION_PROGRESS.md` + - Contains detailed task breakdown, checkboxes, status + +2. **📖 Analysis Document** + - `docs/WEBSOCKET_MIGRATION_ANALYSIS.md` + - Contains architecture, technical specs, code examples + +3. **🔧 Existing Implementation** + - `src/services/realtime.ts` - Current SSE implementation (to be replaced) + - `src/contexts/AuthContext.tsx` - Token management (ready to use) + - `src/contexts/FeaturesContext.tsx` - Feature flags + +4. **📋 Backend Spec** + - `docs/backend-integration-specs-v2.md` - API contracts + - Look for "Real-time Endpoints (SSE)" section (will become WebSocket) + +--- + +## Quick Context Summary + +### What We're Doing +- **Migrating** from SSE (Server-Sent Events) to WebSocket for real-time updates +- **Backend** is implementing AWS API Gateway WebSocket API +- **Frontend** needs to replace EventSource with WebSocket and expand event coverage + +### What Exists +- ✅ Partial SSE infrastructure (goal activity only) +- ✅ Feature flag system (`REACT_APP_FEATURE_REALTIME`) +- ✅ Auth with token storage, refresh, tenant ID +- ✅ Context API state management pattern +- ✅ TypeScript types for all entities +- ✅ Mock SSE server for local testing + +### What's Missing +- ❌ WebSocket implementation (only SSE) +- ❌ Support for action, Measure, issue events +- ❌ Global real-time state management +- ❌ Connection status UI +- ❌ Comprehensive event handlers + +--- + +## Architecture Decision + +**Chosen Approach:** Global Context with React Hooks + +- Single WebSocket connection via `RealtimeContext` +- React hooks for event subscription: `useRealtimeEvent()` +- Follows existing Context API patterns +- Better performance than component-level subscriptions + +--- + +## Next Action (Phase 1) + +Create WebSocket connection manager: + +1. **Create** `src/services/realtime-websocket.ts` + - Implement `RealtimeWebSocket` class + - Connection lifecycle, reconnection, heartbeat + - See `WEBSOCKET_MIGRATION_ANALYSIS.md` section 3.1 for code + +2. **Create** `src/types/realtime.ts` + - All event payload types + - Connection status types + +3. **Update** `env.example` + - Add `REACT_APP_REALTIME_WS_URL` + +--- + +## Backend WebSocket Spec (Quick Reference) + +**URL:** `wss://api.{env}.purposepath.app/realtime?access_token={token}&tenant={tenantId}` + +**Event Types:** +- Goal: `goal.created`, `goal.activated`, `goal.completed`, `goal.cancelled`, `goal.activity.created` +- Action: `action.created`, `action.status_changed`, `action.completed`, `action.priority_changed`, `action.reassigned`, `action.progress_updated` +- Measure: `measure.reading.created` +- Issue: `issue.created`, `issue.status_changed` +- Decision: `decision.created` +- Attachment: `attachment.created` +- System: `ping` (respond with `pong`), `error` + +**Message Format:** +```json +{ + "type": "event_type", + "timestamp": "ISO8601", + "data": { /* event-specific payload */ } +} +``` + +**Error Codes:** +- `UNAUTHORIZED` → refresh token, reconnect +- `RATE_LIMIT_EXCEEDED` → backoff +- `INTERNAL_ERROR` → log and continue + +--- + +## Key Technical Decisions + +1. **Use native WebSocket API** (no external library needed) +2. **Exponential backoff** for reconnection: 1s, 2s, 4s, 8s, 16s, 30s (max) +3. **Token refresh** on UNAUTHORIZED via existing `apiClient.refreshToken()` +4. **Event normalization** from snake_case to camelCase (keep from existing code) +5. **Feature flag** controls connection: `REACT_APP_FEATURE_REALTIME` + +--- + +## How to Resume Work + +1. **Read progress tracker**: `docs/WEBSOCKET_IMPLEMENTATION_PROGRESS.md` +2. **Find current phase**: Check status table and task checkboxes +3. **Review analysis**: `docs/WEBSOCKET_MIGRATION_ANALYSIS.md` for technical details +4. **Create todo list**: Use `manage_todo_list` tool to track session tasks +5. **Update progress**: Mark tasks complete in progress tracker as you go +6. **Update this file**: If architecture changes or new decisions are made + +--- + +## Testing Strategy + +- **Mock Server**: `scripts/mock-sse-server.js` (convert to WebSocket) +- **Unit Tests**: `src/services/__tests__/realtime-websocket.test.ts` +- **Manual Tests**: Connect/disconnect, token expiry, network interruption +- **Target Coverage**: 80%+ + +--- + +## Resources + +### Documentation +- Analysis: `docs/WEBSOCKET_MIGRATION_ANALYSIS.md` +- Progress: `docs/WEBSOCKET_IMPLEMENTATION_PROGRESS.md` +- Backend API: `docs/backend-integration-specs-v2.md` +- Goals Design: `docs/design-frontend-goals-module.md` + +### Key Services +- Auth: `src/services/api.ts` (apiClient) +- Current Realtime: `src/services/realtime.ts` +- Mock Server: `scripts/mock-sse-server.js` + +### Contexts +- Auth: `src/contexts/AuthContext.tsx` +- Features: `src/contexts/FeaturesContext.tsx` +- Limits: `src/contexts/LimitsContext.tsx` +- Subscription: `src/contexts/SubscriptionContext.tsx` +- Planning: `src/contexts/PlanningContext.tsx` + +--- + +## Common Commands + +```bash +# Start dev server +npm start + +# Run tests +npm test + +# Start mock SSE server (will be converted to WebSocket) +npm run sse:mock + +# Run with realtime enabled +$env:REACT_APP_FEATURE_REALTIME="true"; npm start +``` + +--- + +## Update Instructions + +**When completing tasks:** +1. Update checkboxes in `docs/WEBSOCKET_IMPLEMENTATION_PROGRESS.md` +2. Update phase status and progress percentages +3. Add notes about decisions or issues encountered +4. Update "Last Updated" date +5. Add change log entry + +**When session restarts:** +1. Read this file first +2. Read progress tracker +3. Continue from current phase/task + +--- + +**Last Updated:** October 5, 2025 +**Status:** Analysis complete, ready for implementation diff --git a/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_UPDATE_EXAMPLE.md b/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_UPDATE_EXAMPLE.md new file mode 100644 index 00000000..34f7f49b --- /dev/null +++ b/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_UPDATE_EXAMPLE.md @@ -0,0 +1,470 @@ +# Example: Document Update in Action + +**Purpose:** Concrete example of how documents get updated during implementation + +--- + +## Scenario: Implementing Task 1.1 - Create WebSocket Connection Manager + +### Initial State (Before Task) + +**Progress Tracker Status:** +```markdown +## Phase 1: WebSocket Connection Manager ⏳ + +**Status:** Not Started +**Estimated Duration:** 2-3 days + +### Tasks + +- [ ] **Task 1.1:** Create `src/services/realtime-websocket.ts` + - [ ] Implement `RealtimeWebSocket` class + - [ ] Connection lifecycle + ... +``` + +**Session Todo List:** +``` +No todo list found. +``` + +--- + +## Step 1: Session Start - Create Todo List + +### AI Agent Action: +```typescript +manage_todo_list({ + operation: "write", + todoList: [ + { + id: 1, + title: "Create WebSocket connection manager", + description: "Implement src/services/realtime-websocket.ts with full lifecycle management", + status: "in-progress" + }, + { + id: 2, + title: "Create TypeScript types", + description: "Define all event payload types in src/types/realtime.ts", + status: "not-started" + }, + { + id: 3, + title: "Update environment config", + description: "Add REACT_APP_REALTIME_WS_URL to env.example", + status: "not-started" + } + ] +}); +``` + +### AI Response to User: +```markdown +Starting Phase 1: WebSocket Connection Manager + +📋 **Session Plan:** +1. ⏳ Create WebSocket connection manager (in progress) +2. ⏹️ Create TypeScript types +3. ⏹️ Update environment config + +**Current Status:** Phase 1, Task 1.1 +**Overall Progress:** 16% (Phase 0 complete) + +Let's begin by creating the WebSocket connection manager... +``` + +--- + +## Step 2: Implement the Task + +### AI Agent Actions: +```typescript +// 1. Create the file +create_file({ + filePath: "c:\\Projects\\XBS\\PurposePath\\PurposePath_Web\\src\\services\\realtime-websocket.ts", + content: ` +export type ConnectionStatus = + | 'disconnected' + | 'connecting' + | 'connected' + | 'reconnecting' + | 'error'; + +export class RealtimeWebSocket { + // ... full implementation +} +` +}); + +// 2. Immediately update progress tracker (mandatory!) +multi_replace_string_in_file({ + explanation: "Update progress tracker after completing Task 1.1", + replacements: [ + { + explanation: "Mark Task 1.1 complete", + filePath: "c:\\Projects\\XBS\\PurposePath\\PurposePath_Web\\docs\\WEBSOCKET_IMPLEMENTATION_PROGRESS.md", + oldString: `- [ ] **Task 1.1:** Create \`src/services/realtime-websocket.ts\` + - [ ] Implement \`RealtimeWebSocket\` class + - [ ] Connection lifecycle (connect, disconnect, reconnect) + - [ ] Exponential backoff with jitter + - [ ] Ping/pong heartbeat handling + - [ ] Connection state management + - [ ] Event emitter pattern`, + newString: `- [x] **Task 1.1:** Create \`src/services/realtime-websocket.ts\` ✅ **Completed:** Oct 5, 2025 15:30 + - [x] Implement \`RealtimeWebSocket\` class + - [x] Connection lifecycle (connect, disconnect, reconnect) + - [x] Exponential backoff with jitter + - [x] Ping/pong heartbeat handling + - [x] Connection state management + - [x] Event emitter pattern + + **Notes:** Implemented full lifecycle with exponential backoff (1s → 30s max), ping/pong heartbeat every 30s, and UNAUTHORIZED error handling for token refresh.` + }, + { + explanation: "Update phase status from Not Started to In Progress", + filePath: "c:\\Projects\\XBS\\PurposePath\\PurposePath_Web\\docs\\WEBSOCKET_IMPLEMENTATION_PROGRESS.md", + oldString: `## Phase 1: WebSocket Connection Manager ⏳ + +**Status:** Not Started +**Estimated Duration:** 2-3 days`, + newString: `## Phase 1: WebSocket Connection Manager 🔄 + +**Status:** In Progress (20% complete - 1/5 subtasks) +**Started:** October 5, 2025 15:00 +**Estimated Duration:** 2-3 days` + }, + { + explanation: "Update overall progress in status table", + filePath: "c:\\Projects\\XBS\\PurposePath\\PurposePath_Web\\docs\\WEBSOCKET_IMPLEMENTATION_PROGRESS.md", + oldString: `| 0. Analysis & Planning | ✅ Complete | 100% | Oct 5, 2025 | Oct 5, 2025 | +| 1. WebSocket Connection Manager | ⏳ Not Started | 0% | - | - |`, + newString: `| 0. Analysis & Planning | ✅ Complete | 100% | Oct 5, 2025 | Oct 5, 2025 | +| 1. WebSocket Connection Manager | 🔄 In Progress | 20% | Oct 5, 2025 | - |` + }, + { + explanation: "Update overall progress percentage", + filePath: "c:\\Projects\\XBS\\PurposePath\\PurposePath_Web\\docs\\WEBSOCKET_IMPLEMENTATION_PROGRESS.md", + oldString: `**Overall Progress:** 16% (1/6 phases complete)`, + newString: `**Overall Progress:** 20% (1 phase + 20% of phase 1)` + }, + { + explanation: "Update Last Updated timestamp", + filePath: "c:\\Projects\\XBS\\PurposePath\\PurposePath_Web\\docs\\WEBSOCKET_IMPLEMENTATION_PROGRESS.md", + oldString: `**Last Updated:** October 5, 2025`, + newString: `**Last Updated:** October 5, 2025 at 15:30` + }, + { + explanation: "Add change log entry", + filePath: "c:\\Projects\\XBS\\PurposePath\\PurposePath_Web\\docs\\WEBSOCKET_IMPLEMENTATION_PROGRESS.md", + oldString: `## Change Log + +### October 5, 2025 +- **Analysis Phase Complete**: Created comprehensive WebSocket migration analysis`, + newString: `## Change Log + +### October 5, 2025 + +**15:30** - Task 1.1 Complete +- ✅ Created \`src/services/realtime-websocket.ts\` (450 lines) +- Implemented \`RealtimeWebSocket\` class with full lifecycle management +- Features: Connection management, exponential backoff (1s-30s), ping/pong heartbeat, UNAUTHORIZED handling +- Phase 1 now 20% complete (1/5 subtasks) + +**14:00** - Phase 1 Started +- Began WebSocket Connection Manager implementation +- Created session plan with 5 subtasks + +**13:00** - Analysis Phase Complete +- Created comprehensive WebSocket migration analysis` + } + ] +}); + +// 3. Update session todo list +manage_todo_list({ + operation: "write", + todoList: [ + { + id: 1, + title: "Create WebSocket connection manager", + description: "Implement src/services/realtime-websocket.ts with full lifecycle management", + status: "completed" + }, + { + id: 2, + title: "Create TypeScript types", + description: "Define all event payload types in src/types/realtime.ts", + status: "in-progress" + }, + { + id: 3, + title: "Update environment config", + description: "Add REACT_APP_REALTIME_WS_URL to env.example", + status: "not-started" + } + ] +}); +``` + +### AI Response to User: +```markdown +✅ **Task 1.1 Complete** - WebSocket Connection Manager Created + +**File Created:** +- `src/services/realtime-websocket.ts` (450 lines) + +**Features Implemented:** +- ✅ Connection lifecycle management (connect, disconnect, reconnect) +- ✅ Exponential backoff with jitter (1s → 2s → 4s → 8s → 16s → 30s max) +- ✅ Ping/pong heartbeat (every 30 seconds) +- ✅ UNAUTHORIZED error detection and token refresh integration +- ✅ Event emitter pattern for message handling +- ✅ Connection state management with status callbacks + +**Progress Updated:** +- ✅ Progress tracker updated (Task 1.1 checkbox marked) +- ✅ Phase 1 status changed to "In Progress (20%)" +- ✅ Overall progress updated to 20% +- ✅ Change log entry added with timestamp +- ✅ Session todo list updated + +**Phase 1 Progress:** 20% complete (1/5 subtasks) +**Overall Progress:** 20% + +**Next:** Task 1.2 - Create TypeScript types (marked in-progress) + +--- + +Would you like me to continue with Task 1.2, or would you like to review the WebSocket implementation first? +``` + +--- + +## Step 3: Continue to Next Task (Task 1.2) + +### User Response: +``` +Continue with Task 1.2 +``` + +### AI Agent Actions: +```typescript +// 1. Create the types file +create_file({ + filePath: "c:\\Projects\\XBS\\PurposePath\\PurposePath_Web\\src\\types\\realtime.ts", + content: ` +// WebSocket message envelope +export interface WebSocketMessage { + type: string; + timestamp: string; + data: T; +} + +// Connection status +export type ConnectionStatus = + | 'disconnected' + | 'connecting' + | 'connected' + | 'reconnecting' + | 'error'; + +// Goal events +export interface GoalCreatedEvent { + goalId: string; + tenantId: string; + title: string; + ownerId: string; + horizon: string; + status: 'draft' | 'active' | 'completed' | 'paused' | 'cancelled'; + valueTags?: string[]; + createdAt: string; +} + +// ... all other event types +` +}); + +// 2. Update progress tracker +multi_replace_string_in_file({ + explanation: "Update progress tracker after completing Task 1.2", + replacements: [ + { + explanation: "Mark Task 1.2 complete", + filePath: "c:\\Projects\\XBS\\PurposePath\\PurposePath_Web\\docs\\WEBSOCKET_IMPLEMENTATION_PROGRESS.md", + oldString: `- [ ] **Task 1.2:** Create \`src/types/realtime.ts\` + - [ ] \`ConnectionStatus\` type + - [ ] \`WebSocketMessage\` interface + - [ ] All event payload types (goals, actions, Measures, issues) + - [ ] Event handler types`, + newString: `- [x] **Task 1.2:** Create \`src/types/realtime.ts\` ✅ **Completed:** Oct 5, 2025 16:00 + - [x] \`ConnectionStatus\` type + - [x] \`WebSocketMessage\` interface + - [x] All event payload types (goals, actions, Measures, issues) + - [x] Event handler types + + **Notes:** Defined 15+ event types covering all backend events. Used TypeScript discriminated unions for type safety.` + }, + { + explanation: "Update phase progress", + filePath: "c:\\Projects\\XBS\\PurposePath\\PurposePath_Web\\docs\\WEBSOCKET_IMPLEMENTATION_PROGRESS.md", + oldString: `**Status:** In Progress (20% complete - 1/5 subtasks)`, + newString: `**Status:** In Progress (40% complete - 2/5 subtasks)` + }, + { + explanation: "Update overall progress table", + filePath: "c:\\Projects\\XBS\\PurposePath\\PurposePath_Web\\docs\\WEBSOCKET_IMPLEMENTATION_PROGRESS.md", + oldString: `| 1. WebSocket Connection Manager | 🔄 In Progress | 20% | Oct 5, 2025 | - |`, + newString: `| 1. WebSocket Connection Manager | 🔄 In Progress | 40% | Oct 5, 2025 | - |` + }, + { + explanation: "Update overall progress", + filePath: "c:\\Projects\\XBS\\PurposePath\\PurposePath_Web\\docs\\WEBSOCKET_IMPLEMENTATION_PROGRESS.md", + oldString: `**Overall Progress:** 20% (1 phase + 20% of phase 1)`, + newString: `**Overall Progress:** 23% (1 phase + 40% of phase 1)` + }, + { + explanation: "Add change log entry", + filePath: "c:\\Projects\\XBS\\PurposePath\\PurposePath_Web\\docs\\WEBSOCKET_IMPLEMENTATION_PROGRESS.md", + oldString: `### October 5, 2025 + +**15:30** - Task 1.1 Complete`, + newString: `### October 5, 2025 + +**16:00** - Task 1.2 Complete +- ✅ Created \`src/types/realtime.ts\` (250 lines) +- Defined 15+ event payload types using TypeScript discriminated unions +- Covers: Goal, Action, Measure, Issue, Decision, Attachment, System events +- Full type safety for WebSocket message handling +- Phase 1 now 40% complete (2/5 subtasks) + +**15:30** - Task 1.1 Complete` + } + ] +}); + +// 3. Update session todo list +manage_todo_list({ + operation: "write", + todoList: [ + { + id: 1, + title: "Create WebSocket connection manager", + description: "Implement src/services/realtime-websocket.ts with full lifecycle management", + status: "completed" + }, + { + id: 2, + title: "Create TypeScript types", + description: "Define all event payload types in src/types/realtime.ts", + status: "completed" + }, + { + id: 3, + title: "Update environment config", + description: "Add REACT_APP_REALTIME_WS_URL to env.example", + status: "in-progress" + } + ] +}); +``` + +--- + +## Pattern: The Update Loop + +**Every time a task completes, this exact sequence happens:** + +``` +1. Complete the implementation (create/edit files) +2. IMMEDIATELY update progress tracker: + - Mark checkbox complete + - Update phase progress % + - Update overall progress % + - Add change log entry + - Update timestamp +3. Update session todo list +4. Inform user with updated progress +5. Move to next task +``` + +**This ensures:** +- ✅ Documents always in sync +- ✅ Progress always accurate +- ✅ Session recovery always possible +- ✅ User always informed +- ✅ History captured in change log + +--- + +## Verification: End of Session + +### AI Agent Final Actions: + +```typescript +// Check all documents are updated +const progressDoc = await read_file("docs/WEBSOCKET_IMPLEMENTATION_PROGRESS.md"); +const sessionRecovery = await read_file("docs/WEBSOCKET_SESSION_RECOVERY.md"); + +// Verify phase progress +// Expected: Phase 1 at 40% (2/5 tasks complete) +// Expected: Overall at 23% + +// Update session recovery doc with current state +replace_string_in_file({ + filePath: "docs/WEBSOCKET_SESSION_RECOVERY.md", + oldString: `**Phase:** 0 - Analysis & Planning ✅ **COMPLETE** +**Next Phase:** 1 - WebSocket Connection Manager (Not Started) +**Overall Progress:** 16% (1/6 phases)`, + newString: `**Phase:** 1 - WebSocket Connection Manager 🔄 **IN PROGRESS** (40%) +**Next Task:** 1.3 - Update environment configuration +**Overall Progress:** 23%` +}); +``` + +### AI Response to User: + +```markdown +## Session Summary + +**Work Completed:** +1. ✅ Task 1.1 - WebSocket Connection Manager (450 lines) +2. ✅ Task 1.2 - TypeScript Types (250 lines) + +**Files Created:** +- `src/services/realtime-websocket.ts` +- `src/types/realtime.ts` + +**Progress:** +- Phase 1: 40% complete (2/5 tasks) +- Overall: 23% complete + +**Documents Updated:** +- ✅ Progress tracker (2 tasks checked, phase %, change log) +- ✅ Session recovery (current state updated) +- ✅ Session todo list (all current) + +**Next Session:** +- Continue with Task 1.3: Update environment configuration +- Then Task 1.4: Token refresh integration +- Then Task 1.5: Manual testing + +**Resume Command:** +Read `docs/WEBSOCKET_SESSION_RECOVERY.md` to pick up exactly where we left off. + +All progress tracked and saved! 🎯 +``` + +--- + +## Key Takeaway + +**Every single task completion triggers a mandatory document update sequence.** + +This ensures that if the chat session ends at ANY point, the next session can: +1. Read the progress docs +2. See exactly what's done (checkboxes) +3. See exactly what's next +4. Continue seamlessly + +**No progress is ever lost!** 🔒 diff --git a/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_UPDATE_PROTOCOL.md b/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_UPDATE_PROTOCOL.md new file mode 100644 index 00000000..5fd95a87 --- /dev/null +++ b/docs/shared/Specifications/user-app/Websocket/WEBSOCKET_UPDATE_PROTOCOL.md @@ -0,0 +1,448 @@ +# WebSocket Implementation - Update Protocol + +**Purpose:** Ensure documents and progress tracking stay synchronized during implementation + +--- + +## Mandatory Update Workflow + +### 🔄 Before Starting Any Task + +1. **Read current progress** + ``` + Read: docs/WEBSOCKET_IMPLEMENTATION_PROGRESS.md + Find: Current phase and next incomplete task + ``` + +2. **Create session todo list** + ``` + Use: manage_todo_list tool + Add: Tasks planned for this session + Status: Mark first task as "in-progress" + ``` + +3. **Mark task in-progress in progress tracker** + ``` + Update: docs/WEBSOCKET_IMPLEMENTATION_PROGRESS.md + Change: - [ ] Task X.Y → - [ ] Task X.Y ⏳ IN PROGRESS + Add note: "Started: [Date] [Time]" + ``` + +### ✅ After Completing Any Task + +1. **Update session todo list** + ``` + Use: manage_todo_list tool + Mark: Current task as "completed" + Mark: Next task as "in-progress" (if continuing) + ``` + +2. **Update progress tracker immediately** + ``` + Update: docs/WEBSOCKET_IMPLEMENTATION_PROGRESS.md + Change: - [ ] Task X.Y ⏳ → - [x] Task X.Y ✅ + Add note: "Completed: [Date] [Time]" + Document: Any decisions, blockers, or deviations + ``` + +3. **Update phase progress** + ``` + Calculate: % complete for phase (completed tasks / total tasks) + Update: Phase status and progress percentage + Update: "Last Updated" date at top of document + ``` + +4. **Add change log entry** + ``` + Add entry to "Change Log" section: + "### [Date] + - **Task X.Y Complete**: [Brief description] + - **Files Created/Updated**: [List files] + - **Notes**: [Any important notes]" + ``` + +### 🎯 After Completing a Phase + +1. **Update progress tracker** + ``` + Change phase status: ⏳ Not Started → 🔄 In Progress → ✅ Complete + Add completion date + Update overall progress percentage + Calculate total time spent + ``` + +2. **Update session recovery document** + ``` + Update: docs/WEBSOCKET_SESSION_RECOVERY.md + Change: "Current Phase" and "Next Phase" + Update: "Overall Progress" percentage + Update: "Last Updated" date + ``` + +3. **Create phase completion summary** + ``` + Add to change log: + "### Phase X Complete - [Date] + - **Duration**: [X days/hours] + - **Deliverables**: [List files created] + - **Tests Added**: [List test files] + - **Issues Encountered**: [List any issues] + - **Next Phase**: Phase X+1 - [Name]" + ``` + +### 📝 Continuous Documentation Updates + +**When creating new files:** +``` +1. Update progress tracker task checkbox +2. Add file path to "Files Created" in change log +3. Add brief description of file purpose +4. Update relevant section in analysis document if architecture changes +``` + +**When making architectural decisions:** +``` +1. Add note to current task in progress tracker +2. Add entry to "Architectural Decisions" section (if exists) +3. Update session recovery doc if it affects quick reference +4. Document reasoning and alternatives considered +``` + +**When encountering blockers:** +``` +1. Add to "Known Issues & Blockers" section +2. Mark task with blocker emoji: - [ ] Task X.Y ⚠️ BLOCKED +3. Document blocker details and mitigation plan +4. Update status in session todo list +``` + +--- + +## Automated Reminder Checklist + +Use this checklist at key points during implementation: + +### Start of Work Session +``` +□ Read WEBSOCKET_IMPLEMENTATION_PROGRESS.md +□ Read WEBSOCKET_SESSION_RECOVERY.md (if new session) +□ Create session todo list with manage_todo_list +□ Mark first task as in-progress in progress tracker +□ Verify Last Updated date is current +``` + +### After Each Task Completion +``` +□ Update session todo list (mark complete) +□ Update progress tracker checkbox (- [x]) +□ Add completion note with date/time +□ Document any decisions or issues encountered +□ Update phase progress percentage +□ Add change log entry +□ Commit changes to git (if applicable) +``` + +### End of Work Session +``` +□ Update all todo lists (session and progress tracker) +□ Mark in-progress tasks appropriately +□ Add session summary to change log +□ Update "Last Updated" date +□ Save all documents +□ Commit changes to git +``` + +### After Phase Completion +``` +□ Update phase status to ✅ Complete +□ Add phase completion date +□ Calculate and update overall progress % +□ Create phase completion summary in change log +□ Update session recovery document +□ Review and update analysis doc if needed +□ Plan next phase kickoff +``` + +--- + +## Document Synchronization Matrix + +This matrix shows which documents need updating for each type of change: + +| Change Type | Progress Tracker | Session Recovery | Analysis Doc | Change Log | +|-------------|-----------------|------------------|--------------|------------| +| Task started | ✅ Mark in-progress | ❌ | ❌ | ❌ | +| Task complete | ✅ Check box | ❌ | ❌ | ✅ Add entry | +| File created | ✅ Note in task | ❌ | ❌ | ✅ List file | +| Phase complete | ✅ Update status | ✅ Update phase | ❌ | ✅ Summary | +| Architecture change | ✅ Add note | ✅ If major | ✅ Update spec | ✅ Document | +| Blocker encountered | ✅ Mark blocked | ❌ | ❌ | ✅ Add to blockers | +| Decision made | ✅ Add note | ✅ If key decision | ✅ If affects design | ✅ Document reasoning | +| Overall progress | ✅ Update % | ✅ Update % | ❌ | ❌ | + +--- + +## AI Agent Self-Check Protocol + +As an AI agent, I will follow this self-check protocol: + +### Before Each Response Involving Implementation + +1. **Check if task is complete in response** + - If YES → Update progress tracker BEFORE responding to user + - If NO → Note what will be done, update after completion + +2. **Verify documents are in sync** + - Read current progress tracker status + - Verify it matches actual work done + - If out of sync → Update immediately + +3. **Plan updates needed** + - Identify which documents need updates + - Use multi_replace_string_in_file for efficiency + - Update all relevant sections in one operation + +### After Each Implementation Step + +1. **Mandatory progress update** + ```typescript + // After creating file or completing task + await updateProgressTracker({ + taskId: "1.2", + status: "complete", + note: "Created realtime-websocket.ts with full implementation" + }); + ``` + +2. **Verify update success** + - Check that checkbox changed + - Verify note was added + - Confirm date updated + +3. **Document in response** + - Tell user what was updated + - Show updated progress + - Highlight remaining tasks + +### Session Start Protocol + +1. **Read progress documents** + - WEBSOCKET_SESSION_RECOVERY.md (quick context) + - WEBSOCKET_IMPLEMENTATION_PROGRESS.md (detailed status) + +2. **Create session plan** + - Use manage_todo_list tool + - List tasks planned for session + - Mark first task in-progress + +3. **Inform user of status** + - Show current phase and progress + - Highlight next tasks + - Ask for confirmation to proceed + +--- + +## Example Update Workflow + +### Scenario: Completing Task 1.1 (Create realtime-websocket.ts) + +**Step 1: Complete the work** +```typescript +// Create the file with implementation +create_file({ + filePath: "src/services/realtime-websocket.ts", + content: "/* WebSocket implementation */" +}); +``` + +**Step 2: Update progress tracker (REQUIRED)** +```typescript +multi_replace_string_in_file({ + filePath: "docs/WEBSOCKET_IMPLEMENTATION_PROGRESS.md", + replacements: [ + { + // Update task checkbox + oldString: "- [ ] **Task 1.1:** Create `src/services/realtime-websocket.ts`", + newString: "- [x] **Task 1.1:** Create `src/services/realtime-websocket.ts` ✅ Completed: Oct 5, 2025 14:30" + }, + { + // Update Last Updated date + oldString: "**Last Updated:** October 5, 2025", + newString: "**Last Updated:** October 5, 2025 14:30" + }, + { + // Add change log entry + oldString: "## Change Log\n\n### October 5, 2025", + newString: "## Change Log\n\n### October 5, 2025\n- **Task 1.1 Complete**: Created WebSocket connection manager\n- **Files Created**: `src/services/realtime-websocket.ts`\n- **Notes**: Implemented full lifecycle management with reconnection" + } + ] +}); +``` + +**Step 3: Update session todo list** +```typescript +manage_todo_list({ + operation: "write", + todoList: [ + { id: 1, title: "Create WebSocket manager", status: "completed" }, + { id: 2, title: "Create TypeScript types", status: "in-progress" }, + { id: 3, title: "Update env config", status: "not-started" } + ] +}); +``` + +**Step 4: Inform user** +```markdown +✅ **Task 1.1 Complete**: Created `src/services/realtime-websocket.ts` + +**Progress Updated:** +- Progress tracker checkbox marked complete +- Change log entry added +- Session todo list updated + +**Phase 1 Progress:** 20% (1/5 subtasks complete) +**Overall Progress:** 18% (phase 0 + 20% of phase 1) + +**Next:** Task 1.2 - Create TypeScript types (in progress) +``` + +--- + +## Validation Checks + +Before marking a phase complete, verify: + +``` +□ All task checkboxes marked complete +□ Phase status updated to ✅ Complete +□ Completion date added +□ Phase progress = 100% +□ Overall progress % updated +□ Change log has phase completion entry +□ Session recovery doc updated +□ All files mentioned in tasks exist +□ All tests mentioned pass +□ Documentation updated +``` + +--- + +## Recovery from Missed Updates + +If documents fall out of sync: + +1. **Detect the issue** + - Compare actual files created vs. progress tracker + - Check for tasks marked complete but files don't exist + - Look for completed work not reflected in tracker + +2. **Reconcile immediately** + - Create reconciliation change log entry + - Update all missed checkboxes at once + - Add note explaining the gap + - Verify current state is accurate + +3. **Prevent recurrence** + - Follow mandatory update workflow strictly + - Use multi_replace for batch updates + - Always update before responding to user + +--- + +## Tools for Updates + +### Use These Tools Consistently + +1. **manage_todo_list** - Session-level tracking + ```typescript + // Start of session + manage_todo_list({ operation: "write", todoList: [...] }); + + // After each task + manage_todo_list({ operation: "write", todoList: [...] }); // Updated status + ``` + +2. **multi_replace_string_in_file** - Batch document updates + ```typescript + // Update multiple sections at once + multi_replace_string_in_file({ + explanation: "Update progress tracker after completing task 1.1", + replacements: [ + { filePath, oldString, newString, explanation }, + { filePath, oldString, newString, explanation }, + // ... more updates + ] + }); + ``` + +3. **replace_string_in_file** - Single section updates + ```typescript + // For simple updates + replace_string_in_file({ filePath, oldString, newString }); + ``` + +--- + +## Success Metrics + +Progress tracking is successful when: + +- ✅ Documents always reflect current state +- ✅ No tasks marked complete without evidence (files, commits) +- ✅ Change log has entry for every significant change +- ✅ Session recovery doc always accurate for quick restart +- ✅ Phase progress percentages accurate +- ✅ User can see progress at a glance +- ✅ Another AI agent can resume work seamlessly + +--- + +## Quick Reference Commands + +```bash +# Check current progress +cat docs/WEBSOCKET_IMPLEMENTATION_PROGRESS.md | grep "Overall Progress" + +# Check current phase +cat docs/WEBSOCKET_IMPLEMENTATION_PROGRESS.md | grep "Status:" | head -6 + +# Check incomplete tasks +cat docs/WEBSOCKET_IMPLEMENTATION_PROGRESS.md | grep "\- \[ \]" + +# Check last updated +cat docs/WEBSOCKET_IMPLEMENTATION_PROGRESS.md | grep "Last Updated" +``` + +--- + +## Commit Message Templates + +When committing progress updates: + +```bash +# After task completion +git commit -m "feat: Complete task X.Y - [brief description] + +- Created/Updated: [files] +- Progress: Phase X now Y% complete +- Updated: Progress tracker and change log" + +# After phase completion +git commit -m "feat: Complete Phase X - [phase name] + +- All tasks complete +- Deliverables: [list files] +- Progress: Overall XX% complete +- Next: Phase X+1" + +# Progress tracking update only +git commit -m "docs: Update progress tracker + +- Mark task X.Y complete +- Update phase progress to Y% +- Add change log entry" +``` + +--- + +**Remember:** Progress tracking is not optional—it's mandatory for successful implementation and seamless session recovery! diff --git a/docs/shared/Specifications/user-app/account-api.md b/docs/shared/Specifications/user-app/account-api.md new file mode 100644 index 00000000..f8ded1b6 --- /dev/null +++ b/docs/shared/Specifications/user-app/account-api.md @@ -0,0 +1,596 @@ +# Account API Specification + +**Version:** 2.4 +**Last Updated:** January 28, 2026 (Username support in invitation activation + conflict error) +**Service Base URL:** `{REACT_APP_ACCOUNT_API_URL}` (e.g., `https://api.dev.purposepath.app/account/api/v1`) + +## Scope + +Consolidated account endpoints implemented by the Account Lambda controllers: `Auth`, `Users`, `Tenants`, `SubscriptionTiers`, `UserSubscription`, `Billing`, `Subscriptions`, `BillingWebhook`, `Health`. + +## Conventions + +- JSON uses camelCase. Legacy snake_case inputs remain backwards compatible only where noted (logout refresh token). +- Response envelope (`ApiResponse`): `{ "success": true|false, "data": {}, "message": "?", "error": "?", "code": "?" }`. +- Paginated responses use `PaginatedResponse`: same envelope plus `pagination: { page, limit, total, totalPages }`. +- Authenticated endpoints require headers: `Authorization: Bearer {accessToken}`, `X-Tenant-Id: {tenantId}`. Public endpoints are marked. +- Optional headers: `X-Frontend-Base-Url` (used for auth emails), `X-E2E-Test: true` (DEV only to bypass email verification on register). + +## JWT Claims (Issue #545) + +The access token JWT includes the following custom claims: +- `user_id`: User's GUID +- `tenant_id`: Tenant's GUID +- `username`: User's username +- `email_verified`: Boolean (from Person entity) +- `user_status`: User status (e.g., "Active") +- `role`: User role (e.g., "user" or "admin") +- `is_tenant_owner`: Boolean - `true` if the user is the tenant owner, `false` otherwise (Issue #545) + +Frontend can decode the JWT to access these claims, but `isTenantOwner` is also available directly in `AuthResponse.user` for convenience. + +## Authentication + +### POST /auth/login +- Body: `{ "username": "string", "password": "string" }` (username may be email-style; 3-50 chars). +- Response `AuthResponse`: +```json +{ + "success": true, + "data": { + "accessToken": "jwt", + "refreshToken": "jwt", + "user": { + "id": "uuid", + "email": "string", + "firstName": "string", + "lastName": "string", + "personId": "uuid|null", + "tenantId": "uuid", + "status": "string", + "isEmailVerified": true, + "isTenantOwner": false, + "createdAt": "2025-12-29T00:00:00Z", + "updatedAt": "2025-12-29T00:00:00Z" + }, + "person": { "id": "uuid", "firstName": "string", "lastName": "string", "email": "string|null", "phone": "string|null", "title": "string|null" }, + "tenant": { "id": "uuid", "name": "string", "status": "string" } + } +} +``` +- **Issue #545**: `user.isTenantOwner` is `true` if the authenticated user is the tenant owner (`tenant.ownerUserId == user.id`), `false` otherwise. +- Errors: 401 invalid credentials, 403 `EMAIL_NOT_VERIFIED`. + +### POST /auth/google +- Body: `{ "token": "string" }` (Google ID token from OAuth flow). +- Response: same as login (includes `user.isTenantOwner`). +- Notes: Validates token with Google, creates new user/tenant if external identity not found (new user is automatically the tenant owner), or logs in existing user. Auto-updates user avatar from Google profile if user has no avatar. +- **Issue #546**: Allows registration even if Google doesn't provide email (person.email will be null, person.emailVerified will be false). Frontend can detect missing email and direct user to update it via PUT /user/email. + +### POST /auth/microsoft +- Body: `{ "token": "string" }` (Microsoft ID token from OAuth/OIDC flow). +- Response: same as login (includes `user.isTenantOwner`). +- Notes: Validates token with Microsoft OIDC metadata endpoint, creates new user/tenant if external identity not found (new user is automatically the tenant owner), or logs in existing user. Auto-updates user avatar from Microsoft profile if user has no avatar. +- **Issue #546**: Allows registration even if Microsoft doesn't provide email (person.email will be null, person.emailVerified will be false). Frontend can detect missing email and direct user to update it via PUT /user/email. + +### POST /auth/register +- Body: `{ "username": "string", "email": "string", "password": "string", "firstName": "string", "lastName": "string", "phone": "string|null" }`. +- DEV-only bypass: `X-E2E-Test: true` skips email verification. +- Response: `AuthResponse` (auto-login path) or validation error. Email verification links use `X-Frontend-Base-Url` if provided. + +### POST /auth/forgot-password +- Body: `{ "username": "string" }`. +- Response: `{ "success": true, "message": "Password reset email sent" }`. +- Notes: Password reset email is sent to the email address of the Person linked to the username. + +### POST /auth/reset-password +- Body: `{ "token": "string", "newPassword": "string" }`. +- Response: `{ "success": true, "message": "Password reset successfully" }`. + +### POST /auth/refresh +- Body: `{ "refreshToken": "string" }`. +- Response: `{ "success": true, "data": { "accessToken": "string", "refreshToken": "string" } }`. + +### POST /auth/confirm-email +- Body: `{ "token": "string" }`. +- Response: `{ "success": true, "message": "Email confirmed successfully" }`. + +### GET /auth/confirm-email/validate +- Query: `token`. +- Response: `{ "success": true, "data": { "status": "valid|used|expired|not_found|error" } }`. + +### POST /auth/resend-confirmation +- Query: `username` (required - user's username); optional `X-Frontend-Base-Url` for link generation. +- Response: `{ "success": true, "message": "Confirmation email resent" }`. +- Notes: Always returns success even if username not found (security - prevents username enumeration). Email only sent if user exists and needs verification (status is Pending). + +### POST /auth/logout +- Query: `refreshToken` (camelCase). Legacy `refresh_token` still accepted. +- Response: `{ "success": true }`. + +### GET /auth/check-username (Public) +**No Auth Required** - Check if a username is available for registration or invitation activation. + +- Query: `username` (string, required). +- Response (Available): +```json +{ + "success": true, + "data": { + "available": true, + "username": "johndoe" + } +} +``` +- Response (Not Available): +```json +{ + "success": true, + "data": { + "available": false, + "username": "johndoe", + "message": "Username is already taken", + "reasonCode": "TAKEN" + } +} +``` +- Response (Invalid Format): +```json +{ + "success": false, + "error": "Invalid username format", + "code": "VALIDATION_ERROR", + "details": { + "field": "username", + "message": "Username must be 3-50 characters, start with alphanumeric, and contain only alphanumeric, '.', '_', '-', '@'", + "reasonCode": "INVALID_FORMAT" + } +} +``` +- Reason Codes (data.reasonCode when `available: false`): + - `TAKEN`: Username is already in use. + - `RESERVED`: Username is a reserved system name (admin, support, system, etc.). + - `PREVIOUSLY_USED`: Username is reserved for 90 days after a change. + - `CHANGE_RATE_LIMITED`: Authenticated user is within the 30‑day username change cooldown. +- **Notes**: + - This is a public endpoint (no authentication required) for use during registration and invitation activation. + - Returns success with `available: false` for taken usernames (not an error state). + - Returns error only for invalid format or server errors. + - Reserved usernames (admin, support, system, etc.) are considered unavailable. + - If an authenticated user context is available, the response may return `CHANGE_RATE_LIMITED`. + +## Users (Single User Operations) + +### GET /user/{id} +- Path: user ID (GUID). Public auth required. +- Response: `{ "success": true, "data": { "userId": "uuid", "email": "string", "firstName": "string", "lastName": "string", "avatarUrl": "string|null" } }`. + +### GET /user/profile +- Response `UserProfileDetailResponse`: user info with preferences and `personId`. + +### PUT /user/profile +- Body (all optional): `{ "firstName": "string|null", "lastName": "string|null", "phone": "string|null", "avatarUrl": "string|null", "preferences": { "theme": "string", "language": "string", "timezone": "string", "dateFormat": "string", "timeFormat": "string", "currency": "string", "notifications": { "email": true, "push": true, "sms": true, "marketing": true, "coachingReminders": true, "teamUpdates": true, "systemNotifications": true }, "coaching": { "preferredSessionLength": 60, "reminderFrequency": "weekly", "coachingStyle": "directive" } } }`. +- Response: updated `UserProfileDetailResponse`. + +### PUT /user/email +- **Issue #546**: Update user email (for OAuth users who registered without email). +- Requires authentication (including users with unverified email). +- Body: `{ "email": "string" }`. +- Headers: Optional `X-Frontend-Base-Url` for verification email link. +- Response: `{ "success": true, "message": "Email updated successfully. Verification email sent." }`. +- Errors: 400 invalid email format, 409 email already in use (EMAIL_IN_USE), 404 user not found. +- Notes: Updates person.email and sets person.emailVerified to false. Sends verification email using existing /auth/confirm-email flow. Frontend should direct user to email verification screen after successful update. + +### PUT /user/preferences +- Body: `UserPreferencesRequest` (same shape as `preferences` above). +- Response: `{ "success": true, "data": UserPreferencesResponse }`. + +### GET /user/features +- Response: `{ "success": true, "data": ["goals", "operations", ...] }`. + +### GET /user/limits +- Response `UserLimitsResponse`: `{ "goals": 10, "users": null, "projects": null, "apiCallsPerMonth": 10000, "storageMb": 1024 }` (null = unlimited). + +## Tenants + +### GET /tenants/{id} +- Response: `TenantResponse` `{ id, name, status, subscriptionTier, createdAt, updatedAt, userCount, isActive }`. + +### GET /tenants/current +- Response: `TenantResponse`. + +### PUT /tenants/current +- Body: `{ "name": "string|null", "status": "string|null" }`. +- Response: updated `TenantResponse`. + +### GET /tenants/settings +- Response: `{ "success": true, "data": { "targetLineMode": "single|three" } }`. + +### PUT /tenants/settings +- Body: `{ "targetLineMode": "single|three" }`. +- Response: updated settings. + +## Subscription Tiers + +### GET /subscription/tiers (public) +- Response: list of `TierResponse` items: +```json +{ + "success": true, + "data": [ + { + "id": "uuid", + "name": "Starter", + "description": "...", + "features": ["goals", "operations"], + "limits": {"goals": 10, "measures": 50, "actions": null}, + "pricing": {"monthly": 29.99, "yearly": 299.99}, + "supportedFrequencies": ["monthly", "yearly"], + "isActive": true + } + ] +} +``` + +## User Subscription (self-serve) + +### GET /user/subscription +- Returns current tenant subscription with embedded tier, or `data: null` if none. + +### POST /user/subscription +- Body: `{ "tierId": "uuid", "frequency": "monthly|yearly", "promoCode": "string|null", "paymentMethodId": "string|null" }`. +- Response `CreateUserSubscriptionResponse`: `{ "subscription": UserSubscriptionResponse, "requiresPaymentConfirmation": true|false, "clientSecret": "string|null" }`. + +### PUT /user/subscription +- Body: `{ "tierId": "uuid", "frequency": "monthly|yearly", "promoCode": "string|null" }`. +- Response `UpdateUserSubscriptionResponse`: `{ "subscription": UserSubscriptionResponse, "effectiveDate": "ISO-8601" }` (changes take effect end of period). + +### DELETE /user/subscription +- Body: `{ "reason": "string|null", "cancelAtPeriodEnd": true|false }`. +- Response `CancelUserSubscriptionResponse`: `{ "subscription": UserSubscriptionResponse, "cancelEffectiveDate": "ISO-8601" }`. + +### PUT /user/subscription/auto-renewal +- Body: `{ "autoRenewal": true|false }`. +- Response: updated `UserSubscriptionResponse`. + +## Billing (tenant-scoped) + +### PUT /billing/subscription +- Body: `{ "newTier": "Starter|Professional|Enterprise", "effectiveDate": "ISO-8601|null", "prorateBilling": true|false }`. +- Response: `SubscriptionResponse`. + +### POST /billing/payment-intent +- Body: `{ "tierId": "uuid", "frequency": "monthly|yearly", "promoCode": "string|null" }`. +- Response: `{ "success": true, "data": { "clientSecret": "string", "amount": 2999, "currency": "usd" } }`. + +### POST /billing/portal +- Body: `{ "returnUrl": "string|null" }` (defaults to frontend base URL when omitted). +- Response: `{ "success": true, "data": { "url": "https://billing.stripe.com/..." } }`. + +## Subscriptions (admin/ops) + +### GET /subscriptions +- Query: `page`, `pageSize`, `tenantId`, `status`, `tier`, `isTrialing`, `isActive`, `sortBy` (default `CreatedAt`), `sortOrder` (`asc|desc`). +- Response: `PaginatedResponse` with `data` list and `pagination` metadata. + +### GET /subscriptions/{id} +- Path: subscription ID (GUID). Response: `ApiResponse`. + +### GET /subscriptions/tenant/{tenantId} +- Path: tenant ID (GUID). Response: `ApiResponse`. + +### POST /subscriptions +- Body: `{ "tenantId": "uuid", "ownerId": "uuid", "tier": "Starter|Professional|Enterprise", "currentPeriodStart": "ISO-8601|null", "currentPeriodEnd": "ISO-8601|null", "startTrial": true|false, "trialEndsAt": "ISO-8601|null" }`. +- Response: `ApiResponse` (201 Created on success). + +### PUT /subscriptions/{id}/tier +- Body: `{ "newTier": "Starter|Professional|Enterprise", "effectiveDate": "ISO-8601|null", "prorateBilling": true|false }`. +- Response: updated `SubscriptionResponse`. + +### POST /subscriptions/{id}/cancel +- Response: `ApiResponse` (subscription cancelled immediately). + +### POST /subscriptions/{id}/trial +- Body: `{ "trialEndsAt": "ISO-8601" }`. Response: updated `SubscriptionResponse`. + +### PUT /subscriptions/{id}/billing-provider +- Body: `{ "billingProviderId": "string", "providerSubscriptionId": "string", "providerCustomerId": "string" }`. +- Response: updated `SubscriptionResponse` (links provider IDs). + +### POST /subscriptions/promo/validate (public) +- Body: `{ "promoCode": "string", "tierId": "uuid", "frequency": "monthly|yearly" }`. +- Response: `{ "success": true, "data": { "isValid": true|false, "discount": { "type": "percentage|fixed", "value": 20, "duration": "once", "durationInMonths": 6 }, "newPrice": null, "errorMessage": "string|null" } }`. + +### POST /subscriptions/create-payment +- Body: `{ "subscriptionId": "uuid", "paymentProvider": "stripe|paypal|square", "paymentMethodId": "string", "tier": "Starter|Professional|Enterprise", "frequency": "monthly|yearly", "promoCode": "string|null", "metadata": { "key": "value" } }`. +- Response `PaymentSubscriptionResponse`: `{ "providerSubscriptionId": "string", "providerCustomerId": "string", "status": "active|incomplete|trialing|past_due", "clientSecret": "string|null", "requiresAction": true|false, "errorMessage": "string|null" }`. + +## Billing Webhook + +### POST /billing/webhook/{providerId} +- Path: `providerId` (e.g., `stripe`, `paypal`). +- Headers: signature header varies by provider (`Stripe-Signature`, `PayPal-Signature`, or `X-Webhook-Signature`). +- Body: raw webhook payload from provider. +- Response: `{ "success": true, "data": { "received": true } }` (400 on signature/body validation failure). + +## User Invitations (Multi-User System) + +**Owner-Only Operations:** All invitation management endpoints except activation are restricted to tenant owners only. + +### POST /invitations +**Owner-Only** - Invite a person to create a user account. + +- Body: +```json +{ + "personId": "uuid", + "expirationDays": 7 +} +``` +- Headers: `Authorization`, `X-Tenant-Id`, `X-Frontend-Base-Url` (required for activation link). +- Response: +```json +{ + "success": true, + "data": { + "id": "uuid", + "personId": "uuid", + "tenantId": "uuid", + "invitedByUserId": "uuid", + "invitationToken": "64-char-token", + "status": "Sent", + "expiresAt": "2026-01-19T00:00:00Z", + "createdAt": "2026-01-12T00:00:00Z", + "sentAt": "2026-01-12T00:00:00Z" + } +} +``` +- Errors: 403 not owner, 400 person not found/no email/already linked/active invitation exists. + +### GET /invitations +**Owner-Only** - List invitations for tenant. + +- Query: `status` (optional: `Pending|Sent|Accepted|Expired|Withdrawn`). +- Response: +```json +{ + "success": true, + "data": [ + { + "id": "uuid", + "personId": "uuid", + "person": { + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@example.com" + }, + "tenantId": "uuid", + "invitedByUserId": "uuid", + "status": "Sent", + "expiresAt": "2026-01-19T00:00:00Z", + "createdAt": "2026-01-12T00:00:00Z", + "sentAt": "2026-01-12T00:00:00Z", + "acceptedAt": null + } + ] +} +``` + +### POST /invitations/{id}/resend +**Owner-Only** - Regenerate token and resend invitation email. + +- Path: invitation ID (GUID). +- Body: +```json +{ + "expirationDays": 7 +} +``` +- Headers: `X-Frontend-Base-Url` (required for activation link). +- Response: updated invitation object (same as POST /invitations). +- Errors: 403 not owner, 404 not found, 400 already accepted. + +### DELETE /invitations/{id} +**Owner-Only** - Withdraw invitation (cancel before acceptance). + +- Path: invitation ID (GUID). +- Response: `{ "success": true, "message": "Invitation withdrawn successfully" }`. +- Errors: 403 not owner, 404 not found, 400 already accepted. + +### GET /invitations/validate/{token} (Public) +**No Auth Required** - Validate invitation token before activation. + +- Path: invitation token (64-char string). +- Response: +```json +{ + "success": true, + "data": { + "isValid": true, + "invitation": { + "id": "uuid", + "personId": "uuid", + "person": { + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@example.com" + }, + "tenantId": "uuid", + "tenant": { + "id": "uuid", + "name": "Acme Corp", + "status": "Active" + }, + "status": "Sent", + "expiresAt": "2026-01-19T00:00:00Z" + } + } +} +``` +- **Note**: `tenant.name` is populated from the business foundation's company name if available, otherwise falls back to the tenant's name field (Issue #577). +``` +- Invalid token response: +```json +{ + "success": false, + "error": "Invalid or expired invitation", + "data": { "isValid": false } +} +``` + +### POST /invitations/activate (Public) +**No Auth Required** - Activate invitation and create user account. + +- Body (Password-based): +```json +{ + "token": "64-char-token", + "username": "johndoe", + "password": "SecurePassword123!" +} +``` +- Body (OAuth-based): +```json +{ + "token": "64-char-token", + "username": "johndoe", + "googleUserId": "google-oauth-id", + "googleEmail": "john.doe@example.com", + "googleProfilePictureUrl": "https://...", + "username": "johndoe" +} +``` +- **Field Constraints:** + - `username` (optional): 3-50 characters, must start with alphanumeric, can contain alphanumeric + `.` `_` `-` `@`. Regex: `/^[a-zA-Z0-9][a-zA-Z0-9._@-]{2,49}$/`. When omitted, username is auto-generated from email (backward compatibility). + - `password` (password-based only): Required for password activation. + - `googleUserId`, `googleEmail`, `googleProfilePictureUrl` (OAuth-based only): Required for OAuth activation. +- Username rules: + - 3-50 characters + - Must start with a letter or number + - Allowed characters: letters, numbers, `.`, `_`, `-`, `@` + - Reserved usernames are not allowed (e.g., admin, support, system) +- Response: +```json +{ + "success": true, + "data": { + "accessToken": "jwt", + "refreshToken": "jwt", + "user": { + "id": "uuid", + "username": "johndoe", + "personId": "uuid", + "tenantId": "uuid", + "status": "Active", + "createdAt": "2026-01-12T00:00:00Z" + }, + "person": { + "id": "uuid", + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@example.com", + "linkedUserId": "uuid" + }, + "tenant": { + "id": "uuid", + "name": "Acme Corp", + "status": "Active" + } + } +} +``` +- **Note**: `tenant.name` is populated from the business foundation's company name if available, otherwise falls back to the tenant's name field. +- **Email Verification** (Issue #619): When a user activates their account via invitation, their email is automatically marked as verified (`person.isEmailVerified = true`). The invitation acceptance itself serves as email validation, eliminating the need for a separate email verification step. The user's status is set to `Active` immediately upon successful activation. +- Errors: + - 400 `VALIDATION_ERROR`: Invalid token/expired/already used/person already linked/email mismatch (OAuth)/invalid username format. + - 409 `USERNAME_NOT_AVAILABLE`: The specified username is already taken by another user (global uniqueness check). +- **Backward Compatibility**: When `username` is omitted, the system auto-generates a username from the person's email address (existing behavior). + +**Error Responses:** + +```json +{ + "success": false, + "error": "Username is already taken", + "code": "DUPLICATE_RESOURCE", + "details": { + "field": "username" + } +} +``` + +```json +{ + "success": false, + "error": "Username is invalid", + "code": "VALIDATION_ERROR", + "details": { + "field": "username" + } +} +``` + +## Users (Multi-User Operations - Owner Only) + +### GET /users +**Owner-Only** - List all users in tenant. + +- Query: `status` (optional: `Active|Inactive` - omit to return all users). +- Response: +```json +{ + "success": true, + "data": [ + { + "id": "uuid", + "username": "johndoe", + "personId": "uuid", + "person": { + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@example.com" + }, + "status": "Active", + "createdAt": "2026-01-01T00:00:00Z", + "lastLoginAt": "2026-01-12T08:00:00Z" + } + ] +} +``` +- Errors: 403 not owner. + +### GET /users/count +**Owner-Only** - Get count of users in tenant (for billing and management). + +- Query: `status` (optional: `Active|Inactive` - omit to return count of all users). +- Response: +```json +{ + "success": true, + "data": { + "userCount": 5 + } +} +``` +- Errors: 403 not owner. + +### POST /users/{id}/deactivate +**Owner-Only** - Deactivate another user (cannot deactivate self). + +- Path: user ID (GUID). +- Response: `{ "success": true, "message": "User deactivated successfully" }`. +- Errors: 403 not owner, 400 cannot deactivate self, 404 user not found. + +## Health + +### GET /health +- Response: `{ "success": true, "data": { "status": "Healthy", "service": "PurposePath Account Lambda", "version": "x.y.z", "timestamp": "ISO-8601", "environment": "string" } }`. + +### GET /health/detailed +- Response includes `checks` array with component-level statuses; returns 503 when any check fails. + +### GET /health/ready +- Response: `{ "success": true, "data": { "status": "Ready", "timestamp": "ISO-8601", "message": "Service is ready to accept requests" } }`. + +### GET /health/live +- Response: `{ "success": true, "data": { "status": "Alive", "timestamp": "ISO-8601", "message": "Service is running" } }`. diff --git a/docs/shared/Specifications/user-app/account-gap.md b/docs/shared/Specifications/user-app/account-gap.md new file mode 100644 index 00000000..b45f7a24 --- /dev/null +++ b/docs/shared/Specifications/user-app/account-gap.md @@ -0,0 +1,910 @@ +# Account Service Gap Specification + +**Version:** 1.0 +**Status:** Pending Implementation +**Last Updated:** December 30, 2025 +**Base URL:** `{REACT_APP_ACCOUNT_API_URL}` +**Default (Localhost):** `http://localhost:8001` + +--- + +## Overview + +This document specifies additional Account Service endpoints not yet covered in `account-api.md` or `business-foundation-api.md`. These endpoints handle authentication confirmations, subscription management, billing, onboarding, and tenant settings. + +--- + +## Authentication Endpoints + +### POST /auth/resend-confirmation + +Resend email confirmation link. + +**Query Parameters:** + +- `email` - Email address to resend confirmation to + +**Special Headers:** + +- `X-Frontend-Base-Url: {window.location.origin}` (optional, improves email link accuracy) + +**Response:** + +```json +{ + "success": true +} +``` + +**Status Codes:** + +- `200` - Confirmation email sent +- `400` - Invalid email or user not found +- `429` - Rate limited (too many requests) +- `500` - Email sending failed + +--- + +## Subscription Management Endpoints + +### GET /subscription/tiers + +Get all available subscription tiers with pricing (dynamic). + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` (optional for unauthenticated discovery) +- `X-Tenant-Id: {tenantId}` (optional) + +**Response:** + +```json +{ + "success": true, + "data": [ + { + "id": "string", + "name": "string", + "description": "string", + "features": ["goals", "operations", "reports"], + "limits": { + "goals": 10, + "measures": 50, + "actions": null + }, + "pricing": { + "monthly": 29.99, + "yearly": 299.99 + }, + "supportedFrequencies": ["monthly", "yearly"], + "isActive": true + } + ] +} +``` + +**Notes:** + +- Pricing is dynamic and may change +- `supportedFrequencies` indicates which billing cycles are available +- If tier only supports monthly, `yearly` will be `null` +- Both monthly and yearly subscriptions can be cancelled anytime + +**Status Codes:** + +- `200` - Success +- `500` - Server error + +--- + +### GET /user/subscription + +Get current user's subscription details with tier information. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string", + "userId": "string", + "tier": { + "id": "string", + "name": "string", + "features": ["string"], + "limits": {}, + "supportedFrequencies": ["monthly", "yearly"] + }, + "frequency": "monthly|yearly", + "status": "string", + "isActive": true, + "currentPeriodStart": "2025-10-01T00:00:00Z", + "currentPeriodEnd": "2025-11-01T00:00:00Z", + "expirationDate": "string?", + "cancelAtPeriodEnd": false, + "autoRenewal": true, + "price": 29.99, + "currency": "USD", + "isTrial": false, + "trialEndDate": "string?", + "trialExtendedBy": "string?", + "trialExtensionReason": "string?" + } +} +``` + +**Status Codes:** + +- `200` - Success +- `401` - Unauthorized +- `404` - Subscription not found + +--- + +### POST /user/subscription + +Create new subscription (for users without active subscription). + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` +- `Content-Type: application/json` + +**Request:** + +```json +{ + "tierId": "string", + "frequency": "monthly|yearly", + "promoCode": "string?", + "paymentMethodId": "string?" +} +``` + +**Response:** + +```json +{ + "success": true, + "data": { + "subscription": { + "id": "string", + "userId": "string", + "tier": { + "id": "string", + "name": "string", + "features": ["string"], + "limits": {}, + "supportedFrequencies": ["monthly", "yearly"] + }, + "frequency": "monthly|yearly", + "status": "string", + "isActive": true, + "currentPeriodStart": "string (ISO 8601)", + "currentPeriodEnd": "string (ISO 8601)", + "price": "number", + "currency": "string", + "isTrial": "boolean" + }, + "requiresPaymentConfirmation": false, + "clientSecret": "string?" + } +} +``` + +**Notes:** + +- `paymentMethodId` from Stripe Elements payment collection +- `clientSecret` returned if payment requires additional 3D Secure confirmation +- Backend handles Stripe payment processing + +**Status Codes:** + +- `201` - Subscription created +- `400` - Invalid request +- `401` - Unauthorized +- `409` - Subscription already exists + +--- + +### DELETE /user/subscription + +Cancel user subscription. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` +- `Content-Type: application/json` + +**Request:** + +```json +{ + "reason": "string?", + "cancelAtPeriodEnd": true +} +``` + +**Response:** + +```json +{ + "success": true, + "data": { + "subscription": { + "id": "string", + "userId": "string", + "status": "string", + "isActive": false, + "cancelAtPeriodEnd": "boolean", + "currentPeriodEnd": "string (ISO 8601)", + "expirationDate": "string (ISO 8601)" + }, + "cancelEffectiveDate": "2025-11-01T00:00:00Z" + } +} +``` + +**Notes:** + +- If `cancelAtPeriodEnd` is `true`, cancellation at end of billing period +- If `false`, immediate cancellation +- User retains access until `cancelEffectiveDate` + +**Status Codes:** + +- `200` - Success +- `400` - Invalid request +- `401` - Unauthorized +- `404` - Subscription not found + +--- + +### POST /subscription/promo/validate + +Validate promo code and calculate discounted price. + +**Request:** + +```json +{ + "promoCode": "string", + "tierId": "string", + "frequency": "monthly|yearly" +} +``` + +**Response:** + +```json +{ + "success": true, + "data": { + "isValid": true, + "discount": { + "type": "percentage|fixed", + "value": 20, + "duration": "once|repeating|forever", + "durationInMonths": 6 + }, + "newPrice": 23.99 + } +} +``` + +**Status Codes:** + +- `200` - Valid or invalid code (both return 200 with isValid flag) +- `400` - Invalid request format +- `401` - Unauthorized + +--- + +### POST /subscription/trial/start + +Start trial subscription without payment method. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` +- `Content-Type: application/json` + +**Request:** + +```json +{ + "tierId": "string" +} +``` + +**Response:** + +```json +{ + "success": true, + "data": { + "subscription": { + "id": "string", + "userId": "string", + "tier": { + "id": "string", + "name": "string", + "features": ["string"], + "limits": {} + }, + "status": "active", + "isActive": true, + "isTrial": true, + "trialEndDate": "2025-11-13T00:00:00Z", + "price": 0, + "currency": "USD" + }, + "trialEndDate": "2025-11-13T00:00:00Z" + } +} +``` + +**Notes:** + +- Trial duration determined by backend configuration (typically 14 days) +- User can upgrade to paid during or after trial + +**Status Codes:** + +- `201` - Trial created +- `400` - Invalid request +- `401` - Unauthorized +- `409` - Trial already active or subscription exists + +--- + +## Billing Endpoints + +### POST /billing/payment-intent + +Create Stripe payment intent for subscription setup. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` +- `Content-Type: application/json` + +**Request:** + +```json +{ + "tierId": "string", + "frequency": "monthly|yearly", + "promoCode": "string?" +} +``` + +**Response:** + +```json +{ + "success": true, + "data": { + "clientSecret": "pi_xxx_secret_xxx", + "amount": 2999, + "currency": "usd" + } +} +``` + +**Notes:** + +- Amount in cents (2999 = $29.99) +- `clientSecret` used with Stripe Elements for payment collection +- Amount reflects tier pricing with promo code discount applied + +**Status Codes:** + +- `200` - Payment intent created +- `400` - Invalid request +- `401` - Unauthorized +- `500` - Stripe API error + +--- + +### POST /billing/webhook + +Handle Stripe webhooks (internal, not called by frontend). + +**Headers Required:** + +- `Stripe-Signature: t=...,v1=...` (Stripe signature verification) + +**Request Body:** + +```json +{ + "id": "evt_xxxxx", + "object": "event", + "type": "payment_intent.succeeded|charge.refunded|customer.subscription.updated", + "data": { + "object": {} + } +} +``` + +**Response:** + +```json +{ + "success": true, + "received": true +} +``` + +**Notes:** + +- Backend-only endpoint +- Handles async payment confirmations, failures, subscription updates +- Updates subscription status based on Stripe events +- Not called directly by frontend + +**Status Codes:** + +- `200` - Webhook processed +- `400` - Invalid webhook signature +- `500` - Processing error + +--- + +### PUT /user/subscription/auto-renewal + +Update auto-renewal setting for subscription. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` +- `Content-Type: application/json` + +**Request:** + +```json +{ + "autoRenewal": true +} +``` + +**Response:** + +```json +{ + "success": true, + "data": { + "subscription": { + "id": "string", + "userId": "string", + "autoRenewal": true, + "currentPeriodEnd": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)" + } + } +} +``` + +**Status Codes:** + +- `200` - Success +- `400` - Invalid request +- `401` - Unauthorized +- `404` - Subscription not found + +--- + +## Onboarding Endpoints + +### GET /business/onboarding + +Get current user's onboarding data. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "data": { + "businessName": "string", + "website": "string?", + "address": { + "street": "string", + "city": "string", + "state": "string", + "zip": "string", + "country": "string" + }, + "products": [ + { + "id": "string", + "name": "string", + "problem": "string" + } + ], + "step3": { + "niche": "string", + "ica": "string", + "valueProposition": "string" + }, + "step4": { + "coreValues": ["string"], + "coreValuesStatus": "Not started|In progress|Completed", + "purpose": "string", + "purposeStatus": "Not started|In progress|Completed", + "vision": "string", + "visionStatus": "Not started|In progress|Completed" + } + } +} +``` + +**Status Codes:** + +- `200` - Success +- `401` - Unauthorized +- `404` - Onboarding not found + +--- + +### PUT /business/onboarding + +Update onboarding data (partial or complete). + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` +- `Content-Type: application/json` + +**Request:** + +```json +{ + "businessName": "string?", + "website": "string?", + "address": { + "street": "string?", + "city": "string?", + "state": "string?", + "zip": "string?", + "country": "string?" + }, + "products": [ + { + "id": "string?", + "name": "string", + "problem": "string" + } + ], + "step3": { + "niche": "string?", + "ica": "string?", + "valueProposition": "string?" + }, + "step4": { + "coreValues": ["string"], + "coreValuesStatus": "string", + "purpose": "string", + "purposeStatus": "string", + "vision": "string", + "visionStatus": "string" + } +} +``` + +**Response:** + +```json +{ + "success": true, + "data": { + "businessName": "string", + "website": "string?", + "address": { ... }, + "products": [ ... ], + "step3": { ... }, + "step4": { ... } + } +} +``` + +**Products Field Behavior (Smart Merge):** +- Products **with `id`**: Updates existing product +- Products **without `id`** (null/omitted): Creates new product with generated ID +- Products **not in array**: Deleted from business +- **Optional**: Can omit `products` field entirely to leave products unchanged + +**Status Codes:** + +- `200` - Success +- `400` - Invalid request +- `401` - Unauthorized +- `404` - Onboarding not found + +--- + +### PUT /business/onboarding/products + +Bulk update all products - replaces entire product list. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` +- `Content-Type: application/json` + +**Request:** + +```json +{ + "products": [ + { + "id": "string?", + "name": "string", + "problem": "string" + } + ] +} +``` + +**Response:** + +```json +{ + "success": true, + "data": [ + { + "id": "string", + "name": "string", + "problem": "string" + } + ] +} +``` + +**Behavior (Smart Merge):** +- Products **with `id`**: Updates existing product +- Products **without `id`**: Creates new product with generated ID +- Products **not in array**: Deleted from business + +**Use Case:** Replace entire products list in one transaction (e.g., onboarding form submission). + +**Status Codes:** + +- `200` - Success +- `400` - Invalid request +- `401` - Unauthorized +- `404` - Onboarding not found + +--- + +## Business Foundation Endpoints + +### PATCH /business/foundation + +Update a single field of business foundation using field/value pattern. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` +- `Content-Type: application/json` + +**Request:** + +```json +{ + "field": "string (required)", + "value": "any (required)" +} +``` + +**Field Options:** + +| Field | Value Type | Description | +|-------|-----------|-------------| +| `vision` | string | Long-term vision statement | +| `purpose` | string | Company purpose/mission | +| `coreValues` | string[] | Array of core value names | +| `valueProposition` | string | Unique value proposition | +| `businessName` | string | Company name | +| `targetMarket` | string | Target market description | + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string", + "tenantId": "string", + "completionPercentage": "number", + "field": "string", + "value": "any", + "updatedAt": "string (ISO 8601)" + } +} +``` + +**Status Codes:** + +- `200` - Success +- `400` - Invalid field or value +- `401` - Unauthorized +- `404` - Business foundation not found + +--- + +### GET /business/foundation/values/{id} + +Get a specific core value by ID. + +**Path Parameters:** + +- `id` - Core value ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string", + "tenantId": "string", + "businessFoundationId": "string", + "name": "string", + "meaning": "string?", + "implementation": "string?", + "behaviors": ["string"], + "displayOrder": "number", + "isActive": "boolean", + "createdAt": "string (ISO 8601)", + "updatedAt": "string? (ISO 8601)" + } +} +``` + +**Status Codes:** + +- `200` - Success +- `401` - Unauthorized +- `404` - Core value not found + +--- + +## Tenant Settings Endpoints + +### GET /tenants/settings + +Get current tenant settings (Measure configuration, etc). + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` (auto-injected by frontend) + +**Response:** + +```json +{ + "success": true, + "data": { + "targetLineMode": "single" | "three" + } +} +``` + +**Response Fields:** +- `targetLineMode` (string): Measure target line configuration + - `"single"` - Only expected target line (default) + - `"three"` - Expected, optimal, and minimal target lines + +**Status Codes:** +- `200` - Success +- `401` - Unauthorized (missing/invalid token) +- `404` - Tenant not found +- `500` - Server error + +--- + +### PUT /tenants/settings + +Update current tenant settings. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` (auto-injected by frontend) +- `Content-Type: application/json` + +**Request:** + +```json +{ + "targetLineMode": "single" | "three" +} +``` + +**Request Fields:** +- `targetLineMode` (string, required): Target line mode configuration + - Valid values: `"single"` or `"three"` + +**Response:** + +```json +{ + "success": true, + "data": { + "targetLineMode": "single" | "three" + } +} +``` + +**Business Rules:** +- Mode change does NOT affect existing Measure data +- Single mode: UI hides optimal/minimal target inputs +- Three mode: UI shows all three target inputs +- Charts always show expected line; optional lines only if data exists + +**Status Codes:** +- `200` - Success +- `400` - Invalid request (missing field or invalid value) +- `401` - Unauthorized (missing/invalid token) +- `404` - Tenant not found +- `500` - Server error + +--- + +## Error Responses + +All Account Service endpoints follow the standard error format: + +```json +{ + "success": false, + "error": "Human-readable error message", + "code": "ERROR_CODE" +} +``` + +### Common Error Codes + +- `INVALID_CREDENTIALS` - Login failed +- `EMAIL_ALREADY_EXISTS` - Registration with existing email +- `INVALID_TOKEN` - Expired or invalid token +- `SUBSCRIPTION_LIMIT_REACHED` - User at subscription limit +- `PAYMENT_FAILED` - Payment processing error +- `PROMO_CODE_INVALID` - Invalid or expired promo code +- `UNAUTHORIZED` - User lacks required permissions + +--- + +**Navigation:** + +- [← Back to Index](./index.md) +- [Account API Specs →](./account-api.md) +- [Admin API Specs →](./admin-api.md) +- [Business Foundation Specs →](./business-foundation-api.md) diff --git a/docs/shared/Specifications/user-app/business-foundation-api.md b/docs/shared/Specifications/user-app/business-foundation-api.md new file mode 100644 index 00000000..98e31dd1 --- /dev/null +++ b/docs/shared/Specifications/user-app/business-foundation-api.md @@ -0,0 +1,1283 @@ +# Business Foundation API Specification + +**Version:** 1.0 +**Status:** Verified Against Implementation +**Last Updated:** December 30, 2025 +**Base URL:** `/business/foundation` and `/business/onboarding` + +--- + +## Overview + +The Business Foundation API provides complete management of the six strategic pillars: + +1. **Business Profile** - Company facts, industry, stage, size +2. **Core Identity** - Vision, purpose, core values +3. **Target Market** - Niche statement, market size, ideal customer avatars (ICAs) +4. **Products & Services** - Product/service catalog with full details +5. **Value Proposition** - USP, key differentiators, competitive positioning +6. **Business Model** - Revenue streams, pricing, partnerships, distribution + +The API implements 404 fallback pattern: When backend endpoints are not yet available, the frontend returns mock responses to allow UI development to proceed independently. + +--- + +## Authentication & Headers + +All endpoints require: + +``` +Authorization: Bearer {accessToken} +X-Tenant-Id: {tenantId} +Content-Type: application/json +``` + +Both headers must be present in request. Bearer token is obtained from `/auth/login` endpoint. + +--- + +## Data Types & Constants + +### Enums (Allowed Values) + +**CompanyStage** (required in Business Profile) +- `startup` - Finding product-market fit (0-2 years) +- `growth` - Scaling what works (2-5 years) +- `scale` - Expanding markets/products (5-10 years) +- `mature` - Optimization and innovation (10+ years) + +**CompanySize** (required in Business Profile) +- `solo` - 1 person +- `micro` - 2-10 employees +- `small` - 11-50 employees +- `medium` - 51-200 employees +- `large` - 201-1000 employees +- `enterprise` - 1000+ employees + +**RevenueRange** (optional in Business Profile) +- `pre_revenue` +- `under_100k` +- `100k_500k` +- `500k_1m` +- `1m_5m` +- `5m_10m` +- `10m_50m` +- `over_50m` +- `not_disclosed` + +**GeographicFocus** (array, required in Business Profile) +- `local` +- `regional` +- `national` +- `global` + +**ProductType** (required when creating product) +- `product` +- `service` +- `subscription` +- `hybrid` + +**ProductStatus** (required when creating product) +- `active` +- `in_development` +- `planned` +- `retired` + +**PricingTier** (optional for product) +- `premium` +- `mid_market` +- `entry_level` +- `free` + +**PricingModel** (optional for product) +- `one_time` +- `subscription` +- `usage_based` +- `freemium` +- `custom` + +**GrowthTrend** (optional for target market) +- `declining` +- `stable` +- `growing` +- `rapidly_growing` + +**BusinessModelType** (array in Business Model) +- `b2b` - Business to Business +- `b2c` - Business to Consumer +- `b2b2c` +- `marketplace` +- `saas` - Software as a Service +- `consulting` +- `ecommerce` +- `other` + +**SectionStatus** (read-only) +- `empty` - No data provided +- `incomplete` - Partial data provided +- `complete` - Fully populated section + +### Industry Options (for Business Profile) +Technology, Healthcare, Finance, Education, Retail, Manufacturing, Real Estate, Professional Services, Media & Entertainment, Food & Beverage, Transportation, Construction, Agriculture, Energy, Hospitality, Nonprofit, Government, Other + +--- + +## Core Endpoints + +### 1. GET /business/foundation + +**Description:** Retrieve complete business foundation data for current tenant. + +**HTTP Method:** GET + +**Headers:** +- Authorization: Bearer {token} +- X-Tenant-Id: {tenantId} + +**Request Parameters:** None + +**Response:** 200 OK + +```json +{ + "success": true, + "data": { + "id": "string (UUID)", + "tenantId": "string (UUID)", + "profile": { + "businessName": "string", + "businessDescription": "string", + "address": { + "street": "string|null", + "city": "string|null", + "state": "string|null", + "zip": "string|null", + "country": "string (required)" + }, + "industry": "string (must be from INDUSTRY_OPTIONS list)", + "subIndustry": "string|null", + "companyStage": "startup|growth|scale|mature", + "companySize": "solo|micro|small|medium|large|enterprise", + "revenueRange": "pre_revenue|under_100k|100k_500k|500k_1m|1m_5m|5m_10m|10m_50m|over_50m|not_disclosed|null", + "yearFounded": "number|null", + "geographicFocus": ["local|regional|national|global"], + "headquartersLocation": "string|null", + "website": "string|null" + }, + "identity": { + "vision": "string", + "visionTimeframe": "string|null", + "purpose": "string", + "whoWeServe": "string|null", + "values": [ + { + "id": "string (UUID)", + "name": "string", + "meaning": "string", + "implementation": "string", + "behaviors": ["string"] | null, + "displayOrder": "number", + "createdAt": "string (ISO 8601 datetime)", + "updatedAt": "string (ISO 8601 datetime)" + } + ] + }, + "market": { + "nicheStatement": "string", + "marketSize": "string|null", + "growthTrend": "declining|stable|growing|rapidly_growing|null", + "icas": [ + { + "id": "string (UUID)", + "name": "string", + "demographics": "string", + "goals": "string", + "painPoints": "string", + "motivations": "string|null", + "objections": "string|null", + "whereToFind": "string|null", + "buyingProcess": "string|null", + "displayOrder": "number", + "createdAt": "string (ISO 8601 datetime)", + "updatedAt": "string (ISO 8601 datetime)" + } + ] + }, + "products": [ + { + "id": "string (UUID)", + "name": "string", + "tagline": "string|null", + "type": "product|service|subscription|hybrid", + "description": "string", + "problemSolved": "string", + "keyFeatures": ["string"] | null, + "targetAudienceIcaId": "string (UUID) | null", + "pricingTier": "premium|mid_market|entry_level|free|null", + "pricingModel": "one_time|subscription|usage_based|freemium|custom|null", + "differentiators": "string|null", + "status": "active|in_development|planned|retired", + "revenueContribution": "primary|secondary|emerging|null", + "displayOrder": "number", + "createdAt": "string (ISO 8601 datetime)", + "updatedAt": "string (ISO 8601 datetime)" + } + ], + "proposition": { + "uniqueSellingProposition": "string", + "keyDifferentiators": "string", + "proofPoints": ["string"] | null, + "customerOutcomes": "string|null", + "brandPromise": "string|null", + "primaryCompetitors": ["string"] | null, + "competitiveAdvantage": "string|null", + "marketPosition": "leader|challenger|niche|emerging|null" + }, + "model": { + "types": ["b2b|b2c|b2b2c|marketplace|saas|consulting|ecommerce|other"], + "primaryRevenueStream": "string", + "secondaryRevenueStreams": ["string"] | null, + "pricingStrategy": "string|null", + "keyPartners": ["string"] | null, + "distributionChannels": ["string"] | null, + "customerAcquisition": "string|null" + }, + "healthScore": "number (0-100)", + "sectionStatuses": { + "profile": "empty|incomplete|complete", + "identity": "empty|incomplete|complete", + "market": "empty|incomplete|complete", + "products": "empty|incomplete|complete", + "proposition": "empty|incomplete|complete", + "model": "empty|incomplete|complete" + }, + "createdAt": "string (ISO 8601 datetime)", + "updatedAt": "string (ISO 8601 datetime)" + } +} +``` + +**Error Responses:** + +- 401 Unauthorized - Missing or invalid token +- 403 Forbidden - Invalid tenant ID +- 500 Internal Server Error + +**Notes:** +- Returns complete foundation object even if some sections are empty +- `sectionStatuses` indicates which sections have data +- `healthScore` is calculated based on completeness of all sections + +--- + +### 2. PUT /business/foundation + +**Description:** Update complete business foundation (full replacement of entire object). + +**HTTP Method:** PUT + +**Headers:** +- Authorization: Bearer {token} +- X-Tenant-Id: {tenantId} +- Content-Type: application/json + +**Request Body:** + +```json +{ + "profile": { + "businessName": "string", + "businessDescription": "string", + "address": { + "street": "string|null", + "city": "string|null", + "state": "string|null", + "zip": "string|null", + "country": "string" + }, + "industry": "string", + "subIndustry": "string|null", + "companyStage": "startup|growth|scale|mature", + "companySize": "solo|micro|small|medium|large|enterprise", + "revenueRange": "...", + "yearFounded": "number|null", + "geographicFocus": ["local|regional|national|global"], + "headquartersLocation": "string|null", + "website": "string|null" + }, + "identity": {...}, + "market": {...}, + "products": [...], + "proposition": {...}, + "model": {...} +} +``` + +**Response:** 200 OK (same structure as GET response) + +**Validation:** +- `businessName` - Required, max 255 characters +- `businessDescription` - Required, max 2000 characters +- `industry` - Required, must be from INDUSTRY_OPTIONS +- `companyStage` - Required, must be one of enum values +- `companySize` - Required, must be one of enum values +- `geographicFocus` - Required array, min 1 item + +**Error Responses:** +- 400 Bad Request - Invalid data or validation failure +- 401 Unauthorized +- 403 Forbidden +- 422 Unprocessable Entity - Validation failed +- 500 Internal Server Error + +--- + +## Section Update Endpoints (PATCH) + +These endpoints update individual sections without affecting others. They may fall back to legacy endpoints if the specific PATCH endpoint returns 404. + +### 3. PATCH /business/foundation/profile + +**Description:** Update business profile section only (name, industry, stage, size, location). + +**HTTP Method:** PATCH + +**Request Body:** + +```json +{ + "businessName": "string|null", + "businessDescription": "string|null", + "address": { + "street": "string|null", + "city": "string|null", + "state": "string|null", + "zip": "string|null", + "country": "string|null" + }, + "industry": "string|null", + "subIndustry": "string|null", + "companyStage": "startup|growth|scale|mature|null", + "companySize": "solo|micro|small|medium|large|enterprise|null", + "revenueRange": "...|null", + "yearFounded": "number|null", + "geographicFocus": ["..."]|null, + "headquartersLocation": "string|null", + "website": "string|null" +} +``` + +**Response:** 200 OK + +```json +{ + "success": true, + "data": { + "profile": {...}, + "sectionStatuses": { + "profile": "empty|incomplete|complete", + "identity": "...", + "market": "...", + "products": "...", + "proposition": "...", + "model": "..." + }, + "healthScore": "number (0-100)", + "updatedAt": "string (ISO 8601 datetime)" + } +} +``` + +**Fallback Behavior:** +- If endpoint returns 404, falls back to legacy PUT /business/foundation with businessName field +- Returns mock success if legacy endpoint also fails + +**Error Responses:** +- 400 Bad Request +- 401 Unauthorized +- 404 Not Found (triggers fallback) +- 422 Unprocessable Entity + +--- + +### 4. PATCH /business/foundation/identity + +**Description:** Update core identity section (vision, purpose). Use dedicated value endpoints for values array. + +**HTTP Method:** PATCH + +**Request Body:** + +```json +{ + "vision": "string|null", + "visionTimeframe": "string|null", + "purpose": "string|null", + "whoWeServe": "string|null" +} +``` + +**Response:** 200 OK + +```json +{ + "success": true, + "data": { + "identity": { + "vision": "string", + "visionTimeframe": "string|null", + "purpose": "string", + "whoWeServe": "string|null", + "values": [...] + }, + "sectionStatuses": {...}, + "healthScore": "number", + "updatedAt": "string (ISO 8601 datetime)" + } +} +``` + +**Validation:** +- `vision` - Optional, max 1000 characters +- `purpose` - Optional, max 1000 characters +- `visionTimeframe` - Optional, max 255 characters + +**Fallback Behavior:** +- If endpoint returns 404, falls back to legacy PUT /business/foundation with vision and purpose fields + +--- + +### 5. PATCH /business/foundation/market + +**Description:** Update target market section (niche, market size, growth trend). Use dedicated ICA endpoints for icas array. + +**HTTP Method:** PATCH + +**Request Body:** + +```json +{ + "nicheStatement": "string|null", + "marketSize": "string|null", + "growthTrend": "declining|stable|growing|rapidly_growing|null" +} +``` + +**Response:** 200 OK + +```json +{ + "success": true, + "data": { + "market": { + "nicheStatement": "string", + "marketSize": "string|null", + "growthTrend": "...", + "icas": [...] + }, + "sectionStatuses": {...}, + "healthScore": "number", + "updatedAt": "string (ISO 8601 datetime)" + } +} +``` + +**Validation:** +- `nicheStatement` - Optional, max 1000 characters +- `marketSize` - Optional, max 500 characters + +**Fallback Behavior:** +- If endpoint returns 404, falls back to legacy PUT /business/foundation with targetMarket string + +--- + +### 6. PATCH /business/foundation/proposition + +**Description:** Update value proposition section (USP, differentiators, positioning). + +**HTTP Method:** PATCH + +**Request Body:** + +```json +{ + "uniqueSellingProposition": "string|null", + "keyDifferentiators": "string|null", + "proofPoints": ["string"]|null, + "customerOutcomes": "string|null", + "brandPromise": "string|null", + "primaryCompetitors": ["string"]|null, + "competitiveAdvantage": "string|null", + "marketPosition": "leader|challenger|niche|emerging|null" +} +``` + +**Response:** 200 OK + +```json +{ + "success": true, + "data": { + "proposition": {...}, + "sectionStatuses": {...}, + "healthScore": "number", + "updatedAt": "string (ISO 8601 datetime)" + } +} +``` + +**Validation:** +- `uniqueSellingProposition` - Optional, max 500 characters +- `keyDifferentiators` - Optional, max 500 characters +- `primaryCompetitors` - Optional array of strings + +**Fallback Behavior:** +- If endpoint returns 404, falls back to legacy PUT /business/foundation with valueProposition field + +--- + +### 7. PATCH /business/foundation/model + +**Description:** Update business model section (types, revenue streams, pricing, partnerships). + +**HTTP Method:** PATCH + +**Request Body:** + +```json +{ + "types": ["b2b|b2c|b2b2c|marketplace|saas|consulting|ecommerce|other"]|null, + "primaryRevenueStream": "string|null", + "secondaryRevenueStreams": ["string"]|null, + "pricingStrategy": "string|null", + "keyPartners": ["string"]|null, + "distributionChannels": ["string"]|null, + "customerAcquisition": "string|null" +} +``` + +**Response:** 200 OK + +```json +{ + "success": true, + "data": { + "model": {...}, + "sectionStatuses": {...}, + "healthScore": "number", + "updatedAt": "string (ISO 8601 datetime)" + } +} +``` + +**Fallback Behavior:** +- If endpoint returns 404, returns mock success (legacy endpoint doesn't support business model) + +**Error Responses:** Same as other PATCH endpoints + +--- + +## Products Endpoints + +**Base Path:** `/business/foundation/products` + +### 8. POST /business/foundation/products + +**Description:** Create a new product or service. + +**HTTP Method:** POST + +**Request Body:** + +```json +{ + "name": "string (required, max 255)", + "tagline": "string|null (max 255)", + "type": "product|service|subscription|hybrid (required)", + "description": "string (required, max 2000)", + "problemSolved": "string (required, max 1000)", + "keyFeatures": ["string"]|null (max 10 items, each max 200 chars), + "targetAudienceIcaId": "string (UUID)|null", + "pricingTier": "premium|mid_market|entry_level|free|null", + "pricingModel": "one_time|subscription|usage_based|freemium|custom|null", + "differentiators": "string|null (max 500)", + "status": "active|in_development|planned|retired (required)", + "revenueContribution": "primary|secondary|emerging|null" +} +``` + +**Response:** 201 Created + +```json +{ + "success": true, + "data": { + "id": "string (UUID)", + "name": "string", + "tagline": "string|null", + "type": "product|service|subscription|hybrid", + "description": "string", + "problemSolved": "string", + "keyFeatures": ["string"]|null, + "targetAudienceIcaId": "string|null", + "pricingTier": "premium|mid_market|entry_level|free|null", + "pricingModel": "one_time|subscription|usage_based|freemium|custom|null", + "differentiators": "string|null", + "status": "active|in_development|planned|retired", + "revenueContribution": "primary|secondary|emerging|null", + "displayOrder": "number", + "createdAt": "string (ISO 8601 datetime)", + "updatedAt": "string (ISO 8601 datetime)" + } +} +``` + +**Fallback Behavior:** +- If endpoint returns 404, returns mock response with generated temporary ID (format: `temp-{timestamp}`) +- Allows UI to function while backend endpoints are being implemented + +**Error Responses:** +- 400 Bad Request - Missing required fields or validation failure +- 401 Unauthorized +- 403 Forbidden +- 404 Not Found (triggers fallback) +- 422 Unprocessable Entity + +--- + +### 9. PUT /business/foundation/products/{id} + +**Description:** Update an existing product. + +**HTTP Method:** PUT + +**Path Parameters:** +- `id` - Product UUID (required) + +**Request Body:** Same as POST (all fields optional except id in URL) + +```json +{ + "name": "string|null", + "tagline": "string|null", + "type": "product|service|subscription|hybrid|null", + "description": "string|null", + "problemSolved": "string|null", + "keyFeatures": ["string"]|null, + "targetAudienceIcaId": "string|null", + "pricingTier": "...|null", + "pricingModel": "...|null", + "differentiators": "string|null", + "status": "active|in_development|planned|retired|null", + "revenueContribution": "...|null" +} +``` + +**Response:** 200 OK (same structure as POST response) + +**Fallback Behavior:** +- If endpoint returns 404, returns mock response + +**Error Responses:** +- 400 Bad Request +- 401 Unauthorized +- 403 Forbidden +- 404 Not Found (triggers fallback) +- 422 Unprocessable Entity + +--- + +### 10. DELETE /business/foundation/products/{id} + +**Description:** Delete a product. + +**HTTP Method:** DELETE + +**Path Parameters:** +- `id` - Product UUID (required) + +**Request Body:** None + +**Response:** 200 OK + +```json +{ + "success": true, + "data": { + "deleted": true, + "id": "string (UUID)" + } +} +``` + +**Fallback Behavior:** +- If endpoint returns 404, returns mock success response + +**Error Responses:** +- 401 Unauthorized +- 403 Forbidden +- 404 Not Found (triggers fallback) + +--- + +### 11. PUT /business/foundation/products:reorder + +**Description:** Reorder products in display sequence. + +**HTTP Method:** PUT + +**Request Body:** + +```json +{ + "orderedIds": ["string (UUID)", "string (UUID)", ...] +} +``` + +**Response:** 200 OK + +```json +{ + "success": true, + "data": { + "items": [ + { + "id": "string (UUID)", + "displayOrder": "number (0, 1, 2, ...)" + } + ] + } +} +``` + +**Validation:** +- `orderedIds` - Required array, min 1 item +- All IDs must be valid UUIDs +- All IDs must correspond to existing products + +**Fallback Behavior:** +- If endpoint returns 404, returns mock response with reordered items + +**Error Responses:** +- 400 Bad Request - Invalid IDs or format +- 401 Unauthorized +- 403 Forbidden +- 404 Not Found (triggers fallback) + +--- + +## Core Values Endpoints + +**Base Path:** `/business/foundation/values` + +### 12. POST /business/foundation/values + +**Description:** Create a new core value. + +**HTTP Method:** POST + +**Request Body:** + +```json +{ + "name": "string (required, max 100)", + "meaning": "string|null (optional, max 500)", + "implementation": "string|null (optional, max 500)", + "behaviors": ["string"]|null (optional, max 10 items, each max 500 chars), + "displayOrder": "number|null (optional, auto-assigned if not specified)" +} +``` + +**Response:** 201 Created + +```json +{ + "success": true, + "data": { + "id": "string (UUID)", + "tenantId": "string (UUID)", + "businessFoundationId": "string (UUID)", + "name": "string", + "meaning": "string|null", + "implementation": "string|null", + "behaviors": ["string"], + "displayOrder": "number", + "isActive": "boolean", + "createdAt": "string (ISO 8601 datetime)", + "updatedAt": "string (ISO 8601 datetime)" + } +} +``` + +**Fallback Behavior:** +- If endpoint returns 404, returns mock response with generated temporary ID + +**Error Responses:** +- 400 Bad Request +- 401 Unauthorized +- 403 Forbidden +- 404 Not Found (triggers fallback) +- 422 Unprocessable Entity + +--- + +### 13. PUT /business/foundation/values/{id} + +**Description:** Update an existing core value. + +**HTTP Method:** PUT + +**Path Parameters:** +- `id` - Value UUID (required) + +**Request Body:** + +```json +{ + "name": "string|null (max 100 chars)", + "meaning": "string|null (optional, max 500 chars)", + "implementation": "string|null (optional, max 500 chars)", + "behaviors": ["string"]|null (optional, max 10 items, each max 500 chars), + "displayOrder": "number|null (optional)", + "isActive": "boolean|null (optional)" +} +``` + +**Response:** 200 OK + +```json +{ + "success": true, + "data": { + "id": "string (UUID)", + "tenantId": "string (UUID)", + "businessFoundationId": "string (UUID)", + "name": "string", + "meaning": "string|null", + "implementation": "string|null", + "behaviors": ["string"], + "displayOrder": "number", + "isActive": "boolean", + "createdAt": "string (ISO 8601 datetime)", + "updatedAt": "string (ISO 8601 datetime)" + } +} +``` + +**Fallback Behavior:** +- If endpoint returns 404, returns mock response + +**Error Responses:** +- 400 Bad Request +- 401 Unauthorized +- 403 Forbidden +- 404 Not Found (triggers fallback) +- 422 Unprocessable Entity + +--- + +### 14. DELETE /business/foundation/values/{id} + +**Description:** Delete a core value. + +**HTTP Method:** DELETE + +**Path Parameters:** +- `id` - Value UUID (required) + +**Request Body:** None + +**Response:** 200 OK + +```json +{ + "success": true, + "data": { + "id": "string (UUID)", + "deleted": true + } +} +``` + +**Fallback Behavior:** +- If endpoint returns 404, returns mock success response + +**Error Responses:** +- 401 Unauthorized +- 403 Forbidden +- 404 Not Found (triggers fallback) + +--- + +### 15. PUT /business/foundation/values:reorder + +**Description:** Reorder core values in display sequence. + +**HTTP Method:** PUT + +**Request Body:** + +```json +{ + "coreValueIds": ["string (UUID)", "string (UUID)", ...] +} +``` + +**Response:** 200 OK + +```json +{ + "success": true, + "data": [ + { + "id": "string (UUID)", + "tenantId": "string (UUID)", + "businessFoundationId": "string (UUID)", + "name": "string", + "meaning": "string|null", + "implementation": "string|null", + "behaviors": ["string"], + "displayOrder": "number", + "isActive": "boolean", + "createdAt": "string (ISO 8601 datetime)", + "updatedAt": "string (ISO 8601 datetime)" + } + ] +} +``` + +**Validation:** +- `coreValueIds` - Required array, min 1 item + +**Fallback Behavior:** +- If endpoint returns 404, returns mock response + +**Error Responses:** +- 400 Bad Request +- 401 Unauthorized +- 403 Forbidden +- 404 Not Found (triggers fallback) + +--- + +## Ideal Customer Avatar (ICA) Endpoints + +**Base Path:** `/business/foundation/icas` + +### 16. POST /business/foundation/icas + +**Description:** Create a new ideal customer avatar. + +**HTTP Method:** POST + +**Request Body:** + +```json +{ + "name": "string (required, max 100)", + "demographics": "string (required, max 500)", + "goals": "string (required, max 500)", + "painPoints": "string (required, max 500)", + "motivations": "string|null (max 500)", + "objections": "string|null (max 500)", + "whereToFind": "string|null (max 500)", + "buyingProcess": "string|null (max 500)" +} +``` + +**Response:** 201 Created + +```json +{ + "success": true, + "data": { + "id": "string (UUID)", + "name": "string", + "demographics": "string", + "goals": "string", + "painPoints": "string", + "motivations": "string|null", + "objections": "string|null", + "whereToFind": "string|null", + "buyingProcess": "string|null", + "displayOrder": "number", + "createdAt": "string (ISO 8601 datetime)", + "updatedAt": "string (ISO 8601 datetime)" + } +} +``` + +**Fallback Behavior:** +- If endpoint returns 404, returns mock response with generated temporary ID + +**Error Responses:** +- 400 Bad Request +- 401 Unauthorized +- 403 Forbidden +- 404 Not Found (triggers fallback) +- 422 Unprocessable Entity + +--- + +### 17. PUT /business/foundation/icas/{id} + +**Description:** Update an existing ideal customer avatar. + +**HTTP Method:** PUT + +**Path Parameters:** +- `id` - ICA UUID (required) + +**Request Body:** + +```json +{ + "name": "string|null", + "demographics": "string|null", + "goals": "string|null", + "painPoints": "string|null", + "motivations": "string|null", + "objections": "string|null", + "whereToFind": "string|null", + "buyingProcess": "string|null" +} +``` + +**Response:** 200 OK (same structure as POST response) + +**Fallback Behavior:** +- If endpoint returns 404, returns mock response + +**Error Responses:** +- 400 Bad Request +- 401 Unauthorized +- 403 Forbidden +- 404 Not Found (triggers fallback) +- 422 Unprocessable Entity + +--- + +### 18. DELETE /business/foundation/icas/{id} + +**Description:** Delete an ideal customer avatar. + +**HTTP Method:** DELETE + +**Path Parameters:** +- `id` - ICA UUID (required) + +**Request Body:** None + +**Response:** 200 OK + +```json +{ + "success": true, + "data": { + "deleted": true, + "id": "string (UUID)" + } +} +``` + +**Fallback Behavior:** +- If endpoint returns 404, returns mock success response + +**Error Responses:** +- 401 Unauthorized +- 403 Forbidden +- 404 Not Found (triggers fallback) + +--- + +## Health & Wizard Endpoints + +### 19. GET /business/foundation/health + +**Description:** Get foundation health check status. + +**HTTP Method:** GET + +**Request Parameters:** None + +**Response:** 200 OK + +```json +{ + "success": true, + "data": { + "healthScore": "number (0-100)", + "sectionStatuses": { + "profile": "empty|incomplete|complete", + "identity": "empty|incomplete|complete", + "market": "empty|incomplete|complete", + "products": "empty|incomplete|complete", + "proposition": "empty|incomplete|complete", + "model": "empty|incomplete|complete" + }, + "strengths": ["string"], + "opportunities": [ + { + "section": "profile|identity|market|products|proposition|model", + "issue": "string (description of issue)", + "impact": "string (impact of issue)", + "estimatedTime": "string (estimated time to fix)" + } + ], + "recommendations": [ + { + "priority": "number (1=highest)", + "action": "string (recommended action)", + "section": "profile|identity|market|products|proposition|model" + } + ], + "lastUpdated": "string (ISO 8601 datetime)" + } +} +``` + +**Error Responses:** +- 401 Unauthorized +- 403 Forbidden +- 500 Internal Server Error + +--- + +### 20. GET /business/foundation/wizard-progress + +**Description:** Get multi-step wizard completion progress. + +**HTTP Method:** GET + +**Request Parameters:** None + +**Response:** 200 OK + +```json +{ + "success": true, + "data": { + "currentStep": "number (1-6)", + "completedSteps": [1, 2, 3], + "skippedSteps": [], + "isComplete": "boolean", + "lastUpdatedAt": "string (ISO 8601 datetime)" + } +} +``` + +**Notes:** +- `currentStep` - Where user is currently (1=profile, 2=identity, 3=products, 4=market, 5=proposition, 6=review) +- `completedSteps` - Array of step numbers user has completed +- `skippedSteps` - Array of step numbers user has skipped +- `isComplete` - True if all 6 steps completed + +**Error Responses:** +- 401 Unauthorized +- 403 Forbidden +- 500 Internal Server Error + +--- + +### 21. PUT /business/foundation/wizard-progress + +**Description:** Update wizard progress (mark steps complete, move to next step). + +**HTTP Method:** PUT + +**Request Body:** + +```json +{ + "currentStep": "number (1-6)", + "completedSteps": [1, 2, 3], + "skippedSteps": [], + "isComplete": "boolean" +} +``` + +**Response:** 200 OK (same structure as GET response) + +**Validation:** +- `currentStep` - Required, must be 1-6 +- `completedSteps` - Required array of integers 1-6 +- `skippedSteps` - Required array of integers 1-6 +- `isComplete` - Required boolean + +**Frontend Handling:** +- Call this endpoint after user completes each wizard step +- Update `currentStep` to next step number +- Add current step to `completedSteps` array +- Set `isComplete = true` when all steps completed + +**Error Responses:** +- 400 Bad Request - Invalid step numbers +- 401 Unauthorized +- 403 Forbidden +- 422 Unprocessable Entity + +--- + +## Error Handling + +All error responses follow this format: + +```json +{ + "success": false, + "error": "string (error message)" +} +``` + +**Common HTTP Status Codes:** +- 400 - Bad Request (validation failed, malformed JSON) +- 401 - Unauthorized (missing/invalid token) +- 403 - Forbidden (invalid tenant, insufficient permissions) +- 404 - Not Found (endpoint not yet implemented, triggers fallback) +- 422 - Unprocessable Entity (semantic validation failed) +- 500 - Internal Server Error + +**Client Handling:** +- Always check `success` field in response +- On 404, frontend uses mock fallback response +- On 401, refresh token or redirect to login +- On 4xx client errors, display user-friendly error message +- On 500, retry with exponential backoff or display error + +--- + +## Implementation Notes + +### Fallback Pattern +When backend endpoints return 404, the frontend returns mock responses: +- Generated IDs use format: `temp-{timestamp}` +- Mock responses allow UI to function independently +- Data is not persisted to backend, but is available locally +- Once backend endpoint is available, real responses are used + +### Field Naming +- All field names use **camelCase** (not snake_case) +- Dates are ISO 8601 format with timezone +- UUIDs are standard UUID v4 format strings +- Arrays indicated by square brackets `[]` + +### Data Validation +- Max length constraints enforced by backend +- Required fields validated before operation +- Enum values must match exactly +- Nested objects fully validated recursively + +### Rate Limiting +Not specified in this version. Implement per-service requirements. + +### Caching +Clients may cache GET responses but should respect `updatedAt` timestamps and refresh when needed. + +--- + +## Version History + +| Version | Date | Changes | +|---------|------|---------| +| 1.0 | December 30, 2025 | Initial specification verified against complete implementation. All 21 endpoints documented with full request/response payloads, validation rules, and fallback behavior. Includes 4 fallback patterns (PATCH endpoints, products, values, ICAs). | + diff --git a/docs/shared/Specifications/user-app/common-patterns.md b/docs/shared/Specifications/user-app/common-patterns.md new file mode 100644 index 00000000..03b04f76 --- /dev/null +++ b/docs/shared/Specifications/user-app/common-patterns.md @@ -0,0 +1,1188 @@ +# Common Patterns & Data Models + +**Version:** 4.0 +**Last Updated:** December 26, 2025 +**Purpose:** Shared patterns, authentication, error handling, and data models across all services + +[← Back to Index](./index.md) + +## Changelog + +| Version | Date | Changes | +|---------|------|----------| +| 4.0 | December 26, 2025 | **BREAKING:** Standardized all enum values and type definitions to camelCase (e.g., `'not_started'` → `'notStarted'`, `'in_progress'` → `'inProgress'`). Updated pagination query parameter from `limit` to `pageSize`. Added comprehensive data models section with common camelCase field names. | +| 3.0 | October 13, 2025 | Multi-service documentation split | + +--- + +## Authentication and Headers + +### Required Headers + +All protected endpoints require these headers: + +```http +Authorization: Bearer {accessToken} +X-Tenant-Id: {tenantId} +Content-Type: application/json +``` + +### Special Headers + +#### X-Frontend-Base-Url +**Used by:** Account Service auth endpoints that trigger emails + +**Value:** `window.location.origin` + +**Endpoints:** +- `POST /auth/register` +- `POST /auth/resend-confirmation` +- `POST /auth/login` (if `REACT_APP_FE_BASE_HEADER_LOGIN=true`) + +**Purpose:** Backend includes this URL in email links for email verification, password reset, etc. + +**Implementation:** +```typescript +// Automatically added by accountClient interceptor +if (/\/auth\/(register|resend-confirmation)/.test(url)) { + config.headers['X-Frontend-Base-Url'] = window.location.origin; +} +``` + +--- + +## Token Management + +### Storage + +Tokens stored in `localStorage`: + +| Key | Value | Purpose | +|-----|-------|---------| +| `accessToken` | JWT access token | Authentication for API requests | +| `refreshToken` | JWT refresh token | Obtain new access token | +| `tenantId` | Tenant/organization ID | Multi-tenant data isolation | + +### Token Lifecycle + +``` +1. User logs in + ↓ +2. Receive accessToken (15min TTL), refreshToken (30d TTL) + ↓ +3. Store in localStorage + ↓ +4. Include in all API requests via Authorization header + ↓ +5. On 401 response: + a. Call POST /auth/refresh with refreshToken + b. Receive new accessToken + refreshToken + c. Update localStorage + d. Retry original request + ↓ +6. If refresh fails: clear tokens, redirect to login +``` + +### Automatic Token Refresh + +All service clients implement automatic token refresh on 401 responses: + +**Account & Coaching Services** (`api.ts`): +```typescript +this.accountClient.interceptors.response.use( + (response) => response, + async (error) => { + if (error.response?.status === 401) { + await this.refreshToken(); + // Retry original request + } + return Promise.reject(error); + } +); +``` + +**Traction Service** (`traction.ts`): +```typescript +// Queue-based refresh to handle concurrent 401s +traction.interceptors.response.use( + (response) => response, + async (error) => { + if (error.response?.status === 401 && !error.config._retry) { + if (isRefreshing) { + // Queue request during refresh + return new Promise((resolve, reject) => { + pendingQueue.push({resolve, reject}); + }).then((token) => { + error.config.headers.Authorization = `Bearer ${token}`; + return traction.request(error.config); + }); + } + + isRefreshing = true; + error.config._retry = true; + + const ok = await apiClient.refreshToken(); + const token = apiClient.getToken(); + + processQueue(null, token); + + if (ok && token) { + error.config.headers.Authorization = `Bearer ${token}`; + return traction.request(error.config); + } + } + return Promise.reject(error); + } +); +``` + +--- + +## Error Handling + +### Standard Error Response + +All services return consistent error format: + +```json +{ + "success": false, + "error": "Human-readable error message", + "code": "ERROR_CODE", + "details": {} // Optional: Additional context for the error +} +``` + +**Field Descriptions:** +- `success`: Always `false` for error responses +- `error`: Human-readable message describing what went wrong +- `code`: Machine-readable error code (see Common Error Codes table) +- `details`: Optional object with additional error context (field names, validation errors, resource IDs, etc.) + +**Examples:** + +Validation error: +```json +{ + "success": false, + "error": "Email is required", + "code": "VALIDATION_ERROR", + "details": { + "field": "email" + } +} +``` + +Multiple validation errors: +```json +{ + "success": false, + "error": "Validation failed for multiple fields", + "code": "VALIDATION_ERROR", + "details": { + "errors": [ + {"field": "email", "message": "Email is required"}, + {"field": "password", "message": "Password must be at least 8 characters"} + ] + } +} +``` + +Resource not found: +```json +{ + "success": false, + "error": "User not found", + "code": "RESOURCE_NOT_FOUND", + "details": { + "resourceType": "User", + "resourceId": "user_123" + } +} +``` + +### Common Error Codes + +| Code | HTTP Status | Description | Usage | +|------|-------------|-------------|-------| +| `VALIDATION_ERROR` | 400, 422 | Input validation failed | Field validation, format errors | +| `UNAUTHORIZED` | 401 | Authentication required | Missing/invalid token | +| `FORBIDDEN` | 403 | Insufficient permissions | User lacks required access | +| `RESOURCE_NOT_FOUND` | 404 | Resource does not exist | Invalid ID, deleted resource | +| `DUPLICATE_RESOURCE` | 409 | Resource already exists | Unique constraint violation | +| `BUSINESS_RULE_VIOLATION` | 400 | Business logic violation | Invalid operation per domain rules | +| `INTERNAL_SERVER_ERROR` | 500 | Unexpected error | Server-side errors | + +### HTTP Status Codes + +| Code | Meaning | Frontend Action | +|------|---------|----------------| +| 200 | Success | Process response (check `success` field) | +| 401 | Unauthorized | Trigger token refresh, retry request | +| 403 | Forbidden | Show "Access Denied" message | +| 404 | Not Found | Handle missing resource | +| 422 | Validation Error | Show field-specific errors | +| 500 | Server Error | Show error message, enable retry | + +### Frontend Error Handling Patterns + +#### Generic Error Handler +```typescript +try { + const response = await apiCall(); + if (!response.success) { + showError(response.error); + return; + } + // Process response.data +} catch (error: any) { + if (error.response?.status === 401) { + // Handled by interceptor + } else if (error.response?.status === 403) { + showError("You don't have permission to perform this action"); + } else { + showError(error.response?.data?.error || "An error occurred"); + } +} +``` + +#### Validation Error Display +```typescript +if (error.response?.status === 422) { + const validationErrors = error.response.data.errors; + // Display field-specific errors + Object.keys(validationErrors).forEach(field => { + setFieldError(field, validationErrors[field]); + }); +} +``` + +#### Retry Logic +```typescript +async function apiCallWithRetry(maxRetries = 3) { + for (let i = 0; i < maxRetries; i++) { + try { + return await apiCall(); + } catch (error: any) { + if (i === maxRetries - 1) throw error; + if (error.response?.status === 500) { + await delay(Math.pow(2, i) * 1000); // Exponential backoff + continue; + } + throw error; + } + } +} +``` + +--- + +## Data Models and Enumerations + +### Subscription Features + +**Feature Names** (for `/user/features` and tier `features` arrays): + +```typescript +type FeatureName = + | 'goals' // Goals module access + | 'operations' // Operations module access + | 'measures' // Measures module access + | 'strategies' // Strategic planning module access + | 'reports' // Report generation capability + | 'realtime' // Real-time updates via SSE + | 'attachments' // File attachment features + | 'bulkPlanner' // Bulk planning operations + | 'strategyCompare' // Strategy comparison tools + | 'goalCreate'; // Goal creation permissions +``` + +### Subscription Limits + +**Limit Keys** (for `/user/limits` and tier `limits` objects): + +```typescript +type LimitName = + | 'goals' // Maximum number of goals + | 'measures' // Maximum number of Measures + | 'actions' // Maximum number of actions + | 'strategies' // Maximum number of strategies + | 'attachments' // Maximum number of attachments per goal + | 'reports'; // Maximum number of reports per month + +// Limit values: number (max count) | null (unlimited) +``` + +### Goal and Strategy Status + +```typescript +type GoalStatus = 'draft' | 'active' | 'completed' | 'paused' | 'cancelled'; +type StrategyStatus = 'draft' | 'validated' | 'adopted'; +type TimeHorizon = 'year' | 'quarter' | 'month'; +``` + +### Operations Status and Priority + +```typescript +type ActionStatus = 'notStarted' | 'inProgress' | 'completed' | 'blocked' | 'cancelled'; +type ActionPriority = 'low' | 'medium' | 'high' | 'critical'; +type IssueImpact = 'low' | 'medium' | 'high' | 'critical'; +type IssueStatusCategory = 'open' | 'active' | 'inactive' | 'closed'; +``` + +### Activity Types + +```typescript +type ActivityType = 'weeklyReview' | 'note' | 'system' | 'decision' | 'attachment'; +``` + +### Coaching and Insights + +```typescript +type InsightCategory = 'strategy' | 'operations' | 'finance' | 'marketing' | 'leadership'; +type InsightPriority = 'low' | 'medium' | 'high' | 'critical'; +type InsightStatus = 'pending' | 'inProgress' | 'completed'; +``` + +### Onboarding + +```typescript +type OnboardingSuggestionKind = 'niche' | 'ica' | 'valueProposition'; +type OnboardingCoachingTopic = 'coreValues' | 'purpose' | 'vision'; +type OnboardingStatus = 'notStarted' | 'inProgress' | 'completed'; +``` + +### Measure + +```typescript +type MeasureDirection = 'up' | 'down'; // up = higher is better, down = lower is better +``` + +### Subscription + +```typescript +type SubscriptionFrequency = 'monthly' | 'yearly'; +type SubscriptionStatus = string; // Dynamic, e.g., 'active', 'trialing', 'pastDue', 'cancelled' + +// Note: Use isActive boolean for access control, not status string +``` + +### Promo Codes + +```typescript +type PromoDiscountType = 'percentage' | 'fixed'; +type PromoDiscountDuration = 'once' | 'repeating' | 'forever'; + +interface PromoDiscount { + type: PromoDiscountType; + value: number; // Percentage (0-100) or fixed amount in cents + duration: PromoDiscountDuration; + durationInMonths?: number; // For 'repeating' duration +} +``` + +### Payment + +```typescript +type PaymentIntentStatus = + | 'requiresPaymentMethod' + | 'requiresConfirmation' + | 'requiresAction' + | 'processing' + | 'succeeded' + | 'canceled'; +``` + +### Email Confirmation + +```typescript +type TokenStatus = 'valid' | 'used' | 'expired' | 'notFound'; +``` + +### Reports + +```typescript +type ReportFormat = 'pdf' | 'docx'; +``` + +### Real-time Events + +```typescript +type RealtimeEventType = + | 'activityCreated' + | 'decisionCreated' + | 'attachmentCreated' + | 'measureReadingCreated'; +``` + +--- + +## Environment Configuration + +### Required Environment Variables + +```bash +# Service Base URLs +REACT_APP_ACCOUNT_API_URL=https://api.dev.purposepath.app/account/api/v1 +REACT_APP_COACHING_API_URL=https://api.dev.purposepath.app/coaching/api/v1 +REACT_APP_TRACTION_API_URL=https://api.dev.purposepath.app/traction/api/v1 + +# Feature Flags +REACT_APP_MOCK_MODE=false # Global mock mode toggle +REACT_APP_MOCK_ACCOUNT=false # Mock Account Service only +REACT_APP_MOCK_COACHING=false # Mock Coaching Service only +REACT_APP_MOCK_TRACTION=false # Mock Traction Service only +REACT_APP_FEATURE_REALTIME=true # Enable real-time SSE features + +# SSE Configuration +REACT_APP_SSE_BASE_URL=https://api.dev.purposepath.app/traction/api/v1 + +# Optional Features +REACT_APP_FE_BASE_HEADER_LOGIN=false # Add X-Frontend-Base-Url to login requests +``` + +### Environment-Specific URLs + +| Environment | Base URL | +|-------------|----------| +| Development | `https://api.dev.purposepath.app` | +| Staging | `https://api.staging.purposepath.app` | +| Production | `https://api.purposepath.app` | + +### Mock Mode Behavior + +When service-specific mock flags are enabled: +- API calls return realistic mock data +- No network requests made to backend +- Suitable for development and offline testing +- Each service can be mocked independently + +**Example:** +```bash +REACT_APP_MOCK_ACCOUNT=false # Real Account API +REACT_APP_MOCK_COACHING=true # Mock Coaching API +REACT_APP_MOCK_TRACTION=false # Real Traction API +``` + +--- + +## API Response Patterns + +### Success Response + +```typescript +interface ApiResponse { + success: true; + data: T; +} +``` + +### Paginated Response + +```typescript +interface PaginatedResponse { + success: true; + data: T[]; + pagination: { + page: number; + pageSize: number; + total: number; + totalPages: number; + }; +} +``` + +### Error Response + +```typescript +interface ErrorResponse { + success: false; + error: string; + code?: string; + errors?: Record; // Field-specific validation errors +} +``` + +--- + +## Data Models + +### Standard Field Naming + +All API responses use **camelCase** for JSON property names: + +**Common Fields:** +- `accessToken`, `refreshToken` - Authentication tokens +- `userId`, `tenantId` - Entity identifiers +- `firstName`, `lastName` - User name fields +- `avatarUrl` - User profile picture URL +- `createdAt`, `updatedAt` - Timestamps +- `ownerId` - Resource owner identifier +- `valueTags` - Array of value identifiers +- `sharedMeasureId` - Shared Measure identifier +- `thresholdPct` - Percentage threshold value +- `businessImpact` - Business impact description +- `assignedPersonId` - Person assignment identifier +- `newPassword` - New password for updates + +### TypeScript Model Example + +**User Profile** (`api.ts`): +```typescript +interface UserProfile { + userId: string; + email: string; + firstName: string; + lastName: string; + avatarUrl?: string; + createdAt: string; + updatedAt: string; + status: string; + emailVerified: boolean; + preferences: Record; +} +``` + +--- + +## Caching Strategy + +### Client-Side Caching + +**Goals Cache** (`goal-service.ts`): +```typescript +class GoalCache { + private goalListCache: Goal[] | null = null; + private goalCache: Map = new Map(); + + invalidateAll(): void { + this.goalListCache = null; + this.goalCache.clear(); + this.notifyInvalidation(); + } +} +``` + +**Alignment Cache** (`alignment-cache-service.ts`): +```typescript +// Caches alignment calculations to reduce API calls +// Cache key: hash of goal intent + strategies + Measures +// TTL: 5 minutes +``` + +**When to Invalidate:** +- After create/update/delete operations +- On user-triggered refresh +- After certain time periods (TTL) + +--- + +## Optimistic Updates + +Frontend implements optimistic updates for better UX: + +**Pattern** (`optimistic-updates.ts`): +```typescript +async function updateWithOptimism( + optimisticUpdate: () => void, + apiCall: () => Promise, + rollback: () => void +) { + optimisticUpdate(); // Update UI immediately + + try { + await apiCall(); // Make API call + } catch (error) { + rollback(); // Revert on failure + showError(error); + } +} +``` + +**Example:** +```typescript +// Update goal status immediately +setGoalStatus(goalId, 'completed'); + +try { + await goalService.closeGoal(goalId, {finalStatus: 'completed'}); +} catch (error) { + // Revert to previous status + setGoalStatus(goalId, previousStatus); + showError('Failed to close goal'); +} +``` + +--- + +## Real-Time Connection Management + +**SSE Connection** (`realtime.ts`): +```typescript +class RealtimeService { + private connections: Map = new Map(); + + subscribe(goalId: string, callback: (event) => void) { + const url = `${SSE_BASE_URL}/realtime/goals/${goalId}/activity?` + + `accessToken=${token}&tenantId=${tenantId}`; + + const eventSource = new EventSource(url); + + eventSource.addEventListener('activityCreated', callback); + eventSource.addEventListener('error', () => { + // Auto-reconnect with exponential backoff + this.reconnect(goalId, callback); + }); + + this.connections.set(goalId, eventSource); + } + + unsubscribe(goalId: string) { + const connection = this.connections.get(goalId); + if (connection) { + connection.close(); + this.connections.delete(goalId); + } + } +} +``` + +**Connection Features:** +- Auto-reconnect on disconnect +- Exponential backoff for reconnection attempts +- Event deduplication using `lastEventId` +- Graceful fallback to polling if SSE unavailable + +--- + +## Performance Considerations + +### Request Batching + +Batch-related entity queries: +```typescript +// Instead of N requests +for (const actionId of actionIds) { + await getActionGoals(actionId); +} + +// Make 1 request +const goalsMap = await getActionGoalRelationships({actionIds}); +``` + +### Debouncing + +Debounce expensive operations: +```typescript +const debouncedAlignment = debounce(async () => { + await calculateAlignment(goal); +}, 1000); // Wait 1s after last change +``` + +### Lazy Loading + +Load data on-demand: +```typescript +// Load goals list immediately +const goals = await getGoals(); + +// Load full goal details only when opened +const goalDetails = await getGoalById(selectedGoalId); +``` + +--- + +## Health Check Endpoints (Kubernetes Patterns) + +### Overview + +All services implement standard Kubernetes health check patterns for infrastructure monitoring, orchestration, and load balancing. These endpoints provide different levels of health information for various use cases. + +### Service Availability + +| Service | Base Route | Endpoints | +|---------|-----------|-----------| +| Account Service | `/health` | All 4 health check types | +| Traction Service | `/health` | All 4 health check types | + +### Endpoints + +#### 1. GET /health - Basic Health Check + +**Purpose:** Lightweight availability check for load balancers and uptime monitoring + +**Authentication:** None (public endpoint) + +**Request:** No parameters + +**Response:** `ApiResponse` (200 OK) + +```json +{ + "success": true, + "data": { + "status": "Healthy", + "service": "PurposePath Account Lambda", + "version": "1.0.0", + "timestamp": "2025-10-17T12:00:00Z", + "environment": "production" + } +} +``` + +**Use Cases:** + +- Load balancer health checks +- Basic uptime monitoring +- Service discovery verification +- Quick availability checks + +**Check Frequency:** High (every 5-10 seconds) + +--- + +#### 2. GET /health/detailed - Detailed Health Status + +**Purpose:** Comprehensive health check with dependency status and diagnostics + +**Authentication:** None (public endpoint) + +**Request:** No parameters + +**Response:** `ApiResponse` (200 OK or 503 Service Unavailable) + +**Success Response (200):** + +```json +{ + "success": true, + "data": { + "status": "Healthy", + "service": "PurposePath Traction Lambda", + "version": "1.0.0", + "timestamp": "2025-10-17T12:00:00Z", + "environment": "production", + "checks": [ + { + "component": "API", + "status": "Healthy", + "description": "API is responsive" + }, + { + "component": "Application", + "status": "Healthy", + "description": "Application is running normally" + }, + { + "component": "Configuration", + "status": "Healthy", + "description": "Environment: production" + } + ] + } +} +``` + +**Failure Response (503):** +```json +{ + "success": true, + "data": { + "status": "Unhealthy", + "service": "PurposePath Account Lambda", + "version": "1.0.0", + "timestamp": "2025-10-17T12:00:00Z", + "environment": "production", + "checks": [ + { + "component": "Application", + "status": "Unhealthy", + "description": "Database connection failed" + } + ] + } +} +``` + +**Use Cases:** + +- Monitoring dashboards (Datadog, New Relic, CloudWatch) +- Detailed service health debugging +- Dependency health tracking +- Root cause analysis during incidents + +**Check Frequency:** Medium (every 30-60 seconds) + +--- + +#### 3. GET /health/ready - Readiness Probe + +**Purpose:** Kubernetes readiness probe - determines if pod should receive traffic + +**Authentication:** None (public endpoint) + +**Request:** No parameters + +**Response:** 200 OK or 503 Service Unavailable + +**Success Response (200):** + +```json +{ + "success": true, + "data": { + "status": "Ready", + "timestamp": "2025-10-17T12:00:00Z", + "message": "Service is ready to accept requests" + } +} +``` + +**Failure Response (503):** + +```json +{ + "success": false, + "error": "Service not ready" +} +``` + +**Kubernetes Behavior:** + +- **200 response:** Pod added to service load balancer pool (receives traffic) +- **503 response:** Pod removed from load balancer pool (stops receiving traffic) +- **Does NOT restart pod** (unlike liveness probe) +- Pod stays running but excluded from traffic routing + +**Use Cases:** + +- Zero-downtime deployments +- Gradual rollouts and canary deployments +- Prevents traffic to initializing pods +- Warm-up period handling +- Database connection establishment +- Cache warming + +**Check Frequency:** High (every 10 seconds) + +**Example Kubernetes Configuration:** + +```yaml +readinessProbe: + httpGet: + path: /health/ready + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + successThreshold: 1 + failureThreshold: 3 +``` + +--- + +#### 4. GET /health/live - Liveness Probe + +**Purpose:** Kubernetes liveness probe - determines if pod should be restarted + +**Authentication:** None (public endpoint) + +**Request:** No parameters + +**Response:** 200 OK (always successful unless service is completely unresponsive) + +**Response (200):** + +```json +{ + "success": true, + "data": { + "status": "Alive", + "timestamp": "2025-10-17T12:00:00Z", + "message": "Service is running" + } +} +``` + +**Kubernetes Behavior:** + +- **200 response:** Pod continues running normally +- **No response or timeout:** Kubernetes **restarts the pod** +- **More lenient than readiness** (higher thresholds) +- Used to detect deadlocks, memory leaks, or unrecoverable states + +**Use Cases:** + +- Automatic recovery from deadlocks +- Memory leak detection and recovery +- Thread hang detection +- Self-healing infrastructure +- Recovery from unrecoverable errors + +**Check Frequency:** Low (every 30-60 seconds) + +**Example Kubernetes Configuration:** + +```yaml +livenessProbe: + httpGet: + path: /health/live + port: 8080 + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 3 +``` + +--- + +### Health Check Comparison Table + +| Endpoint | Purpose | K8s Feature | Failure Behavior | Check Frequency | Authentication | +|----------|---------|-------------|------------------|-----------------|----------------| +| `/health` | Basic availability | Load Balancer | None (monitoring only) | High (5-10s) | None | +| `/health/detailed` | Diagnostics | Observability | None (monitoring only) | Medium (30-60s) | None | +| `/health/ready` | Traffic routing | Readiness Probe | Remove from LB pool | High (10s) | None | +| `/health/live` | Pod health | Liveness Probe | **Restart pod** | Low (30-60s) | None | + +### Readiness vs Liveness + +**Key Difference:** + +```text +Readiness: "Can I send traffic to this pod?" +Liveness: "Should I restart this pod?" +``` + +**Example Scenario:** + +1. Pod starts → Readiness fails (503) → No traffic sent +2. Pod initializes → Readiness passes (200) → Traffic starts flowing +3. Pod runs fine → Both probes pass (200) +4. Database connection lost → Readiness fails (503) → Traffic stops, **pod stays running** +5. Database reconnects → Readiness passes (200) → Traffic resumes +6. Pod deadlocks → Liveness fails (no response) → **Kubernetes restarts pod** + +### Monitoring Integration + +#### Datadog Integration + +```yaml +# datadog-agent.yaml +checks: + - name: purposepath_health + url: https://api.purposepath.app/health/detailed + method: GET + interval: 60 + alert_on_failure: true +``` + +#### New Relic Synthetic Monitor + +```javascript +// New Relic Synthetic Script +const assert = require('assert'); + +$http.get('https://api.purposepath.app/health/detailed', function(err, response, body) { + assert.equal(response.statusCode, 200, 'Expected 200 OK'); + const data = JSON.parse(body); + assert.equal(data.data.status, 'Healthy', 'Service should be healthy'); + + data.data.checks.forEach(check => { + assert.equal(check.status, 'Healthy', `${check.component} should be healthy`); + }); +}); +``` + +#### CloudWatch Alarms + +```bash +# Create CloudWatch alarm for health check failures +aws cloudwatch put-metric-alarm \ + --alarm-name purposepath-account-unhealthy \ + --alarm-description "Alert when Account service is unhealthy" \ + --metric-name HealthCheckStatus \ + --namespace PurposePath/Services \ + --statistic Average \ + --period 60 \ + --threshold 1 \ + --comparison-operator LessThanThreshold \ + --evaluation-periods 3 +``` + +### Frontend Integration Example + +Frontend can use health checks for status pages or admin dashboards: + +```typescript +// Status page component +async function checkServiceHealth() { + try { + const accountHealth = await fetch('/api/v1/health'); + const tractionHealth = await fetch('/api/v1/health'); + + return { + account: accountHealth.ok ? 'operational' : 'degraded', + traction: tractionHealth.ok ? 'operational' : 'degraded' + }; + } catch (error) { + return { + account: 'down', + traction: 'down' + }; + } +} + +// Admin dashboard with detailed status +async function getDetailedServiceStatus() { + const response = await fetch('/api/v1/health/detailed'); + const data = await response.json(); + + return { + status: data.data.status, + version: data.data.version, + checks: data.data.checks, + timestamp: data.data.timestamp + }; +} +``` + +### Complete Kubernetes Deployment Example + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: purposepath-account +spec: + replicas: 3 + selector: + matchLabels: + app: purposepath-account + template: + metadata: + labels: + app: purposepath-account + spec: + containers: + - name: account-service + image: purposepath/account:latest + ports: + - containerPort: 8080 + + # Readiness probe - controls traffic routing + readinessProbe: + httpGet: + path: /health/ready + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + successThreshold: 1 + failureThreshold: 3 + + # Liveness probe - controls pod restart + livenessProbe: + httpGet: + path: /health/live + port: 8080 + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 3 + + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" +--- +apiVersion: v1 +kind: Service +metadata: + name: purposepath-account +spec: + selector: + app: purposepath-account + ports: + - port: 80 + targetPort: 8080 + type: LoadBalancer +``` + +### Best Practices + +1. **Readiness Probe:** + + - Check all critical dependencies (database, cache, external APIs) + - Return 503 if any critical dependency is unavailable + - Use shorter timeouts and more frequent checks + - Should recover automatically when dependencies recover + +2. **Liveness Probe:** + + - Keep checks simple and fast + - Only fail for unrecoverable errors (deadlocks, memory exhaustion) + - Use longer timeouts and less frequent checks + - Avoid checking external dependencies (use readiness for that) + +3. **Detailed Health:** + + - Include all dependency statuses + - Add latency metrics + - Include version information + - Log failures for debugging + +4. **Basic Health:** + + - Keep extremely lightweight + - Return quickly + - Minimal dependencies + - Suitable for high-frequency polling + +--- + +## Security Best Practices + +1. **Never log sensitive data:** + + ```typescript + // Bad + console.log('Token:', accessToken); + + // Good + console.log('Token:', accessToken.slice(0, 10) + '...'); + ``` + +2. **Validate on both client and server:** + + - Client validation for UX + - Server validation for security + +3. **Handle token expiration gracefully:** + + - Auto-refresh on 401 + - Clear tokens and redirect on refresh failure + +4. **Use HTTPS in production:** + + - All API endpoints must use HTTPS + - No mixed content + +5. **Sanitize user input:** + + - Especially in rich text editors + - Prevent XSS attacks + +--- + +**Navigation:** + +- [← Back to Index](./index.md) +- [← Account Service](./account-service.md) +- [← AI/Coaching Service](../ai-user/backend-integration-unified-ai.md) +- [← Traction Service](./traction-service/README.md) diff --git a/docs/shared/Specifications/user-app/dashboard-service.md b/docs/shared/Specifications/user-app/dashboard-service.md new file mode 100644 index 00000000..9ec3a1e7 --- /dev/null +++ b/docs/shared/Specifications/user-app/dashboard-service.md @@ -0,0 +1,1394 @@ +# Dashboard API Specification + +**Services:** Account Service (configuration) + Traction Service (widget data) +**Version:** 1.2 +**Last Updated:** January 12, 2026 + +## Endpoints Summary + +| Method | Endpoint | Service | Description | +|--------|----------|---------|-------------| +| GET | `/dashboard/config` | Account | Get user's dashboard configuration | +| PUT | `/dashboard/config` | Account | Save user's dashboard configuration | +| DELETE | `/dashboard/config` | Account | Reset dashboard to default | +| GET | `/dashboard/templates` | Account | Get available dashboard templates | +| GET | `/dashboard/templates/{id}` | Account | Get specific template details | +| POST | `/dashboard/config/apply-template` | Account | Apply template to user's dashboard | +| GET | `/dashboard/widgets` | Account | Get widget catalog (available widget definitions) | +| GET | `/dashboard/widgets/{widgetId}/instances/{instanceId}/settings` | Traction | Get widget instance settings | +| PUT | `/dashboard/widgets/{widgetId}/instances/{instanceId}/settings` | Traction | Save widget instance settings | +| POST | `/dashboard/widgets/{widgetId}/instances/{instanceId}/settings/reset` | Traction | Reset widget instance settings to defaults | +| GET | `/dashboard/widgets/{widgetId}/settings/schema` | Traction | Get widget settings schema (for UI generation + defaults) | +| GET | `/dashboard/widgets/hot-list/data` | Traction | Get hot list widget data | +| GET | `/dashboard/widgets/recent-activity/data` | Traction | Get recent activity widget data | +| GET | `/dashboard/widgets/action-list/data` | Traction | Get action list widget data | +| GET | `/dashboard/widgets/issue-list/data` | Traction | Get issue list widget data | +| GET | `/dashboard/widgets/goal-progress/data` | Traction | Get goal progress widget data | +| GET | `/dashboard/widgets/measure-graph/data` | Traction | Get measure graph widget data | +| GET | `/dashboard/widgets/actions-by-status/data` | Traction | Get actions by status widget data | +| GET | `/dashboard/widgets/ai-insights/data` | Traction | Get AI insights widget data | +| GET | `/dashboard/widgets/performance-score/data` | Traction | Get performance score widget data | +| GET | `/dashboard/widgets/team-alignment/data` | Traction | Get team alignment widget data | + +## Revisions + +| Date | Version | Summary | +|------|---------|---------| +| January 12, 2026 | 1.2 | Add widget settings schema + reset endpoint; include widget version in settings payloads; clarify instanceId behavior for system default templates | +| January 9, 2026 | 1.1 | Split specification by service: Account (config/templates) and Traction (widget data) | +| January 8, 2026 | 1.0 | Initial specification for customizable dashboard system | + +--- + +## Overview + +The Dashboard API provides endpoints for managing user-customizable dashboards with drag-and-drop widgets. The API is split across two services: + +- **Account Service**: Manages dashboard configuration and templates (user preferences, layout state) +- **Traction Service**: Provides operational data for widgets (actions, goals, measures, issues, etc.) + +### Key Concepts + +- **Dashboard Config**: A user's dashboard layout state (widgets + layout) +- **Widget Definition**: A widget type from the catalog (e.g., "action-list", "measure-graph") +- **Widget Instance**: A specific widget placed on a user's dashboard +- **Widget Settings**: Per-widget-instance configuration used by Traction to generate widget data +- **Layout**: Position and size of widgets in a responsive grid +- **Template**: Pre-configured dashboard layouts users can apply + +--- + +## Service Configuration + +### Account Service (Configuration & Templates) + +**Base URL:** `/account/api/v1` +**Base Path:** `/dashboard` + +**Required Headers:** +```http +Authorization: Bearer {accessToken} +X-Tenant-Id: {tenantId} +Content-Type: application/json +``` + +### Traction Service (Widget Data) + +**Base URL:** `/traction/api/v1` +**Base Path:** `/dashboard/widgets` + +**Required Headers:** +```http +Authorization: Bearer {accessToken} +X-Tenant-Id: {tenantId} +Content-Type: application/json +``` + +--- + +# Account Service Endpoints + +## Get Dashboard Configuration + +**GET** `/account/api/v1/dashboard/config` + +Retrieve the authenticated user's dashboard configuration. Returns the default configuration if no custom configuration exists. + +### Response Structure + +```typescript +interface DashboardConfigResponse { + success: boolean; + data: { + id: string; + userId: string; + name: string; + isDefault: boolean; + widgets: WidgetInstanceResponse[]; + layouts: ResponsiveLayouts; + createdAt: string; // ISO 8601 + updatedAt: string; // ISO 8601 + }; +} + +interface WidgetInstanceResponse { + instanceId: string | null; // Frontend-owned unique ID for this widget instance (may be null for system default templates) + layoutId?: string; // Response-only, transient. Present only when instanceId is null; generated by backend; NOT persisted. + widgetId: string; // References widget catalog ID + title?: string; // Custom title override +} + +interface ResponsiveLayouts { + lg: WidgetLayout[]; // >= 1200px + md: WidgetLayout[]; // >= 996px + sm: WidgetLayout[]; // >= 768px + xs: WidgetLayout[]; // < 768px +} + +interface WidgetLayout { + i: string; // Matches WidgetInstance.instanceId (or WidgetInstance.layoutId when instanceId is null) + x: number; // X position (0-based grid column) + y: number; // Y position (0-based grid row) + w: number; // Width in grid units + h: number; // Height in grid units +} +``` + +### Example Request + +```bash +GET /account/api/v1/dashboard/config +Authorization: Bearer {token} +X-Tenant-Id: {tenantId} +``` + +### Example Response + +```json +{ + "success": true, + "data": { + "id": "config_abc123", + "userId": "user_xyz789", + "name": "My Dashboard", + "isDefault": false, + "widgets": [ + { + "instanceId": "hot-list-1704700800000", + "widgetId": "hot-list", + "title": null + } + ], + "layouts": { + "lg": [ + { "i": "hot-list-1704700800000", "x": 0, "y": 0, "w": 2, "h": 2 } + ], + "md": [ + { "i": "hot-list-1704700800000", "x": 0, "y": 0, "w": 2, "h": 2 } + ], + "sm": [ + { "i": "hot-list-1704700800000", "x": 0, "y": 0, "w": 3, "h": 2 } + ], + "xs": [ + { "i": "hot-list-1704700800000", "x": 0, "y": 0, "w": 2, "h": 2 } + ] + }, + "createdAt": "2026-01-08T10:00:00Z", + "updatedAt": "2026-01-08T15:30:00Z" + } +} +``` + +### Business Rules + +- Returns default configuration if user has no saved config +- Configuration is user-specific (isolated by userId within tenant) +- Widget instances must reference valid widget IDs from the catalog +- **Frontend owns widget instance IDs** (`widgets[].instanceId`) +- When returning a system default template (no user config exists), `widgets[].instanceId` may be null + - When `instanceId` is null, the backend must provide `widgets[].layoutId` and `layouts[].i` must match that layoutId + - The frontend must immediately generate `instanceId`s for those widgets, update layouts to use the new instanceIds, and persist via `PUT /account/api/v1/dashboard/config` + - Settings endpoints require a non-null instanceId (settings cannot be read/updated until the frontend assigns an instanceId) + - `layoutId` is **transient** and **response-only**: it is used only to correlate `widgets[]` with `layouts[]` in the default template response and is never persisted +- Layout positions must be non-negative integers + +--- + +## Save Dashboard Configuration + +**PUT** `/account/api/v1/dashboard/config` + +Save or update the authenticated user's dashboard configuration. + +### Request Body + +```typescript +interface SaveDashboardConfigRequest { + name?: string; // Dashboard name (default: "My Dashboard") + widgets: SavedWidgetInstance[]; + layouts: ResponsiveLayouts; +} + +interface SavedWidgetInstance { + instanceId: string; // Required (frontend-owned). Must be non-null for saved configs. + widgetId: string; // References widget catalog ID + title?: string; // Custom title override +} +``` + +### Example Request + +```bash +PUT /account/api/v1/dashboard/config +Authorization: Bearer {token} +X-Tenant-Id: {tenantId} +Content-Type: application/json +``` + +```json +{ + "name": "Executive Dashboard", + "widgets": [ + { + "instanceId": "hot-list-1704700800000", + "widgetId": "hot-list" + } + ], + "layouts": { + "lg": [ + { "i": "hot-list-1704700800000", "x": 0, "y": 0, "w": 2, "h": 2 } + ], + "md": [ + { "i": "hot-list-1704700800000", "x": 0, "y": 0, "w": 2, "h": 2 } + ], + "sm": [ + { "i": "hot-list-1704700800000", "x": 0, "y": 0, "w": 3, "h": 2 } + ], + "xs": [ + { "i": "hot-list-1704700800000", "x": 0, "y": 0, "w": 2, "h": 2 } + ] + } +} +``` + +### Response + +```json +{ + "success": true, + "data": { + "id": "config_abc123", + "userId": "user_xyz789", + "name": "Executive Dashboard", + "isDefault": false, + "widgets": [...], + "layouts": {...}, + "createdAt": "2026-01-08T10:00:00Z", + "updatedAt": "2026-01-08T16:45:00Z" + } +} +``` + +### Validation Rules + +| Field | Rule | +|-------|------| +| `widgets` | Required, non-empty array | +| `widgets[].instanceId` | Required (frontend-owned), unique within dashboard | +| `widgets[].widgetId` | Required, must exist in widget catalog | +| `layouts` | Required, must have lg, md, sm, xs | +| `layouts[].i` | Must match a widget instanceId | +| `layouts[].x` | >= 0, < grid columns (5 for lg) | +| `layouts[].y` | >= 0 | +| `layouts[].w` | >= widget minW, <= widget maxW | +| `layouts[].h` | >= widget minH, <= widget maxH | + +### Error Responses + +**400 Bad Request - Invalid Widget** +```json +{ + "success": false, + "error": "Invalid widget ID: unknown-widget", + "code": "VALIDATION_ERROR", + "details": { + "field": "widgets[0].widgetId", + "value": "unknown-widget" + } +} +``` + +**403 Forbidden - Widget Not Accessible** +```json +{ + "success": false, + "error": "Widget 'team-alignment' requires Professional tier or higher", + "code": "FORBIDDEN", + "details": { + "widgetId": "team-alignment", + "requiredTier": "professional", + "currentTier": "starter" + } +} +``` + +--- + +## Reset Dashboard Configuration + +**DELETE** `/account/api/v1/dashboard/config` + +Reset the user's dashboard to the system default configuration. + +### Example Request + +```bash +DELETE /account/api/v1/dashboard/config +Authorization: Bearer {token} +X-Tenant-Id: {tenantId} +``` + +### Response + +```json +{ + "success": true, + "data": { + "id": "default", + "userId": "user_xyz789", + "name": "Default Dashboard", + "isDefault": true, + "widgets": [...], + "layouts": {...}, + "createdAt": "2026-01-09T00:00:00Z", + "updatedAt": "2026-01-09T00:00:00Z" + } +} +``` + +--- + +## Get Available Templates + +**GET** `/account/api/v1/dashboard/templates` + +Retrieve all available dashboard templates the user can access based on their subscription tier. + +### Query Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `category` | string | Filter by category: `starter`, `executive`, `operations` | + +### Response Structure + +```typescript +interface DashboardTemplatesResponse { + success: boolean; + data: DashboardTemplate[]; +} + +interface DashboardTemplate { + id: string; + name: string; + description: string; + category: 'starter' | 'executive' | 'operations' | 'custom'; + tier: 'free' | 'starter' | 'professional' | 'enterprise'; + previewImageUrl?: string; + widgetCount: number; + isAccessible: boolean; // Based on user's subscription +} +``` + +### Example Request + +```bash +GET /account/api/v1/dashboard/templates?category=executive +Authorization: Bearer {token} +X-Tenant-Id: {tenantId} +``` + +### Example Response + +```json +{ + "success": true, + "data": [ + { + "id": "template_starter_basic", + "name": "Getting Started", + "description": "A simple dashboard with essential widgets to get you started", + "category": "starter", + "tier": "free", + "previewImageUrl": "/assets/templates/starter-basic.png", + "widgetCount": 3, + "isAccessible": true + }, + { + "id": "template_exec_overview", + "name": "Executive Overview", + "description": "High-level view of goals, measures, and team performance", + "category": "executive", + "tier": "professional", + "previewImageUrl": "/assets/templates/exec-overview.png", + "widgetCount": 6, + "isAccessible": true + } + ] +} +``` + +--- + +## Get Template Details + +**GET** `/account/api/v1/dashboard/templates/{id}` + +Retrieve detailed configuration for a specific template. + +### Path Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `id` | string | Template identifier | + +### Response + +```json +{ + "success": true, + "data": { + "id": "template_exec_overview", + "name": "Executive Overview", + "description": "High-level view of goals, measures, and team performance", + "category": "executive", + "tier": "professional", + "previewImageUrl": "/assets/templates/exec-overview.png", + "widgets": [ + { + "widgetId": "hot-list", + "settings": { + "showPastDueActions": true, + "showAtRiskMeasures": true, + "showCriticalIssues": true, + "maxItems": 5 + }, + "layout": { "x": 0, "y": 0, "w": 2, "h": 2 } + } + ], + "isAccessible": true + } +} +``` + +### Error Responses + +**404 Not Found** +```json +{ + "success": false, + "error": "Template not found", + "code": "RESOURCE_NOT_FOUND" +} +``` + +--- + +## Apply Template + +**POST** `/account/api/v1/dashboard/config/apply-template` + +Apply a template to the user's dashboard, replacing their current configuration. + +### Request Body + +```typescript +interface ApplyTemplateRequest { + templateId: string; +} +``` + +### Example Request + +```bash +POST /account/api/v1/dashboard/config/apply-template +Authorization: Bearer {token} +X-Tenant-Id: {tenantId} +Content-Type: application/json +``` + +```json +{ + "templateId": "template_exec_overview" +} +``` + +### Response + +```json +{ + "success": true, + "data": { + "id": "config_abc123", + "userId": "user_xyz789", + "name": "Executive Overview", + "isDefault": false, + "widgets": [...], + "layouts": {...}, + "createdAt": "2026-01-09T10:00:00Z", + "updatedAt": "2026-01-09T10:00:00Z" + } +} +``` + +--- + +## Get Widget Catalog + +**GET** `/account/api/v1/dashboard/widgets` + +Retrieve the complete widget catalog with all available widget definitions. This provides metadata about widgets including their categories, descriptions, size constraints, and accessibility. The frontend uses this as the single source of truth for available widgets. + +**Note**: Widget accessibility is determined by the backend based on the user's current subscription tier and enabled features. Refresh strategies and intervals are implementation details handled by individual widget components, not part of the catalog definition. + +### Example Request + +```bash +GET /account/api/v1/dashboard/widgets +Authorization: Bearer {token} +X-Tenant-Id: {tenantId} +``` + +### Response + +Response contains a standard envelope with `success` boolean and `data` array of widget definitions. + +```json +{ + "success": true, + "data": [ + { + "id": "hot-list", + "name": "Hot List", + "description": "View urgent items: past due actions, at-risk measures, and critical issues", + "category": "overview", + "tags": ["urgent", "alerts", "priority", "at-risk"], + "isAccessible": true, + "previewImage": "/assets/widgets/hot-list-preview.png", + "size": { + "defaultW": 2, + "defaultH": 3, + "minW": 2, + "minH": 2, + "maxW": 3, + "maxH": 5 + } + }, + { + "id": "action-list", + "name": "Action List", + "description": "Filtered and sorted list of actions with customizable views", + "category": "actions", + "tags": ["actions", "tasks", "todo", "list"], + "isAccessible": true, + "size": { + "defaultW": 3, + "defaultH": 4, + "minW": 2, + "minH": 3, + "maxW": 5, + "maxH": 6 + } + }, + { + "id": "team-alignment", + "name": "Team Alignment", + "description": "Measure team alignment across values, goals, and collaboration", + "category": "team", + "tags": ["team", "alignment", "collaboration", "department"], + "isAccessible": false, + "size": { + "defaultW": 3, + "defaultH": 3, + "minW": 2, + "minH": 2, + "maxW": 4, + "maxH": 4 + } + } + ] +} +``` + +### Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `data` | array | yes | Array of widget definition objects | +| `data[].id` | string | yes | Unique widget identifier (e.g., "hot-list", "action-list"). Must be unique and stable across system updates | +| `data[].name` | string | yes | Display name for the widget | +| `data[].description` | string | yes | Description shown in widget gallery | +| `data[].category` | string | yes | Widget category. Must be one of: `overview`, `actions`, `goals`, `measures`, `issues`, `insights`, `team` | +| `data[].tags` | array of strings | no | Optional free-form tags for search/filtering. Common examples: "urgent", "alerts", "priority", "actions", "tasks", "team", "alignment" | +| `data[].isAccessible` | boolean | yes | Indicates if current user can access this widget. Determined by backend based on user's subscription tier and enabled features | +| `data[].previewImage` | string | no | Optional preview image URL for widget gallery | +| `data[].size` | object | yes | Size constraints in grid units | +| `data[].size.defaultW` | number | yes | Default width in grid units when adding widget (1-12) | +| `data[].size.defaultH` | number | yes | Default height in grid rows when adding widget | +| `data[].size.minW` | number | yes | Minimum width in grid columns (1-12) | +| `data[].size.minH` | number | yes | Minimum height in grid rows | +| `data[].size.maxW` | number | yes | Maximum width in grid columns (1-12) | +| `data[].size.maxH` | number | yes | Maximum height in grid rows | + +### Widget Categories + +| Category | Description | Example Widgets | +|----------|-------------|-----------------| +| `overview` | Summary/aggregation widgets | Hot List, Performance Score | +| `actions` | Action management widgets | Action List, Actions By Status | +| `goals` | Goal tracking widgets | Goal Progress | +| `measures` | Metric visualization widgets | Measure Graph | +| `issues` | Issue management widgets | Issue List | +| `insights` | AI/coaching insights widgets | AI Insights | +| `team` | Team analytics widgets | Team Alignment | + +### Field Constraints + +- **Widget ID**: Must be unique, alphanumeric with hyphens, stable across system updates +- **Category**: Must exactly match one of the valid category values listed above +- **Size constraints**: + - All size values must be positive integers + - `minW` and `maxW` must be between 1 and 12 (grid columns) + - `minW` ≤ `defaultW` ≤ `maxW` + - `minH` ≤ `defaultH` ≤ `maxH` +- **Tags**: Array of strings, optional. Used for search/filtering only. No validation rules on tag values. +- **Preview Image**: Must be a valid URL if provided + +### Business Rules + +- Returns all widgets available in the system, regardless of user's subscription tier +- `isAccessible` field is calculated by backend based on: + - User's current subscription tier + - Enabled feature flags for the user + - Backend-evaluated access rules +- Widget IDs must remain stable across updates (used in saved dashboard configurations) +- Size constraints use grid units (default grid is 5 columns for large screens, see Grid Configuration section) +- Descriptions and metadata can be updated dynamically without frontend code changes +- Refresh strategies and intervals are widget implementation details handled in widget code, not included in catalog + +### Error Responses + +**401 Unauthorized** +```json +{ + "success": false, + "error": "Unauthorized", + "code": "UNAUTHORIZED" +} +``` + +**500 Internal Server Error** +```json +{ + "success": false, + "error": "Internal server error", + "code": "INTERNAL_SERVER_ERROR" +} +``` + +--- + +# Traction Service Endpoints + +## Widget Instance Settings + +Widget settings are stored per user and per widget instance. Widgets use these settings to determine what data to return. + +Dashboard config (Account Service) does not store widget settings. + +### Get Widget Instance Settings + +**GET** `/traction/api/v1/dashboard/widgets/{widgetId}/instances/{instanceId}/settings` + +Retrieve current settings for a specific widget instance. + +If no settings exist yet, returns an empty `settings` object. **Missing fields imply default operational behavior** (either a default value or a default rule handled by the widget/backend), e.g.: +- If a filter setting is missing, show all items +- If a "next X days" setting is missing, default to 7 days (widget-defined) + +The `version` returned is the widget's current version (used to interpret settings and schema). + +### Response + +```json +{ + "success": true, + "data": { + "widgetId": "hot-list", + "instanceId": "widget-1704700800000-abc123", + "version": 1, + "settings": {}, + "updatedAt": "2026-01-12T12:00:00Z" + } +} +``` + +### Save Widget Instance Settings + +**PUT** `/traction/api/v1/dashboard/widgets/{widgetId}/instances/{instanceId}/settings` + +Persist settings for a specific widget instance. + +### Request Body + +```json +{ + "version": 1, + "settings": { + "maxItemsPerSection": 5, + "showUrgentIssues": true + } +} +``` + +### Response + +```json +{ + "success": true, + "data": { + "widgetId": "hot-list", + "instanceId": "widget-1704700800000-abc123", + "version": 1, + "settings": { + "maxItemsPerSection": 5, + "showUrgentIssues": true + }, + "updatedAt": "2026-01-12T12:05:00Z" + } +} +``` + +--- + +### Reset Widget Instance Settings + +**POST** `/traction/api/v1/dashboard/widgets/{widgetId}/instances/{instanceId}/settings/reset` + +Reset settings for a widget instance to its defaults as defined by the widget's settings schema. + +- If the widget defines no defaults, the backend returns an empty `settings` object. +- The backend persists the reset result so subsequent calls return the same values. + +### Response + +```json +{ + "success": true, + "data": { + "widgetId": "hot-list", + "instanceId": "widget-1704700800000-abc123", + "version": 1, + "settings": { + "maxItemsPerSection": 5, + "showUrgentIssues": true + }, + "updatedAt": "2026-01-12T12:10:00Z" + } +} +``` + +--- + +## Widget Settings Schema + +**GET** `/traction/api/v1/dashboard/widgets/{widgetId}/settings/schema` + +Return the widget's **settings UI schema** (including defaults) so the frontend can auto-generate a standardized settings dialog. + +Notes: +- **Schema is never null.** Widgets with no settings return `fields: []`. +- Settings represent **operational configuration only** (not layout/position/size). +- The settings dialog is schema-driven by default. Widgets may opt into a **custom settings dialog** via `ui.mode = "custom"`. +- Frontend uses `defaultValue` to prefill the dialog when a setting key is missing from stored settings. + +### Response Structure + +```typescript +type SettingFieldType = + | 'text' + | 'number' + | 'select' + | 'multiselect' + | 'checkbox' + | 'daterange' + | 'color' + | 'entity-picker'; + +interface SettingFieldValidation { + min?: number; + max?: number; + minLength?: number; + maxLength?: number; + pattern?: string; // regex string + message?: string; // optional override message +} + +interface SettingFieldOption { + value: string; + label: string; + disabled?: boolean; + description?: string; +} + +interface SettingFieldSchema { + key: string; // setting key within settings object + label: string; // UI label + type: SettingFieldType; // UI control type + description?: string; // help text + required?: boolean; // required in the UI + defaultValue?: unknown; // default UI value (may be null or omitted if default is "behavioral") + placeholder?: string; + options?: SettingFieldOption[]; // for select/multiselect + entityType?: 'goal' | 'measure' | 'action' | 'issue' | 'user' | 'strategy'; // for entity-picker + multiple?: boolean; // for entity-picker + validation?: SettingFieldValidation; + dependsOn?: { field: string; value: unknown }; // conditional visibility +} + +interface WidgetSettingsSchemaResponse { + success: boolean; + data: { + widgetId: string; + version: number; // widget settings schema version + ui: { mode: 'schema' | 'custom' }; // settings dialog rendering strategy + fields: SettingFieldSchema[]; // schema-driven fields (empty when none) + }; +} +``` + +### Response + +```json +{ + "success": true, + "data": { + "widgetId": "hot-list", + "version": 1, + "ui": { + "mode": "schema" + }, + "fields": [ + { + "key": "maxItemsPerSection", + "label": "Max items per section", + "type": "number", + "description": "Maximum items shown in each section", + "required": false, + "defaultValue": 5, + "validation": { "min": 1, "max": 50 } + }, + { + "key": "showUrgentIssues", + "label": "Show urgent issues", + "type": "checkbox", + "description": "Include urgent issues in the Hot List", + "required": false, + "defaultValue": true + } + ] + } +} +``` + +--- + +## Hot List Widget + +**GET** `/traction/api/v1/dashboard/widgets/hot-list/data` + +Get urgent items: past due actions, at-risk measures, and critical issues. + +### Query Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `instanceId` | string | Yes | Widget instance ID. Settings are loaded by this id. | + +### Response + +```json +{ + "success": true, + "data": { + "pastDueActions": [ + { + "id": "action_001", + "title": "Complete Q4 budget review", + "daysOverdue": 3, + "priority": "high", + "assigneeName": "John Doe" + } + ], + "atRiskMeasures": [ + { + "id": "m1", + "name": "Monthly Revenue", + "variance": -15, + "goalTitle": "Achieve $1.2M ARR" + } + ], + "criticalIssues": [ + { + "id": "issue_001", + "title": "Production outage", + "impact": "critical", + "daysOpen": 1, + "reporterName": "Alice Chen" + } + ] + } +} +``` + +--- + +## Recent Activity Widget + +**GET** `/traction/api/v1/dashboard/widgets/recent-activity/data` + +Get recent activities across the system. + +### Query Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `instanceId` | string | Yes | Widget instance ID. Settings are loaded by this id. | + +### Response + +```json +{ + "success": true, + "data": [ + { + "id": "activity_001", + "type": "decision", + "title": "Decision made", + "description": "Decided to focus on enterprise customers", + "entityType": "goal", + "entityId": "goal_001", + "entityName": "Achieve $1.2M ARR", + "createdAt": "2026-01-08T09:15:00Z", + "userName": "Jane Smith" + } + ] +} +``` + +--- + +## Action List Widget + +**GET** `/traction/api/v1/dashboard/widgets/action-list/data` + +Get a filtered and sorted list of actions. + +### Query Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `instanceId` | string | Yes | Widget instance ID. Settings are loaded by this id. | + +### Response + +```json +{ + "success": true, + "data": { + "actions": [ + { + "id": "action_001", + "title": "Review marketing campaign", + "dueDate": "2026-01-10T17:00:00Z", + "priority": "high", + "status": "in_progress", + "progress": 60, + "assigneeName": "John Doe", + "goalTitle": "Achieve $1.2M ARR" + } + ], + "total": 25 + } +} +``` + +--- + +## Issue List Widget + +**GET** `/traction/api/v1/dashboard/widgets/issue-list/data` + +Get a filtered list of issues. + +### Query Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `instanceId` | string | Yes | Widget instance ID. Settings are loaded by this id. | + +### Response + +```json +{ + "success": true, + "data": { + "issues": [ + { + "id": "issue_001", + "title": "Customer churn increasing", + "impact": "high", + "priority": "high", + "status": "investigating", + "reporterName": "Customer Success", + "affectedGoalsCount": 2, + "createdAt": "2026-01-05T10:00:00Z" + } + ], + "total": 15 + } +} +``` + +--- + +## Goal Progress Widget + +**GET** `/traction/api/v1/dashboard/widgets/goal-progress/data` + +Get goal progress with strategies and measures. + +### Query Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `instanceId` | string | Yes | Widget instance ID. Settings are loaded by this id. | + +### Response + +```json +{ + "success": true, + "data": { + "goals": [ + { + "id": "goal_001", + "title": "Achieve $1.2M ARR", + "progress": 75, + "expectedProgress": 70, + "status": "on_track", + "ownerName": "Jane Smith", + "strategies": [ + { + "id": "strat_001", + "title": "Expand marketing", + "progress": 65 + } + ], + "measures": [ + { + "id": "measure_revenue", + "name": "Monthly Revenue", + "currentValue": 95000, + "targetValue": 100000, + "unit": "USD", + "isPrimary": true + } + ] + } + ], + "summary": { + "total": 5, + "onTrack": 3, + "atRisk": 1, + "behind": 1, + "completed": 0 + } + } +} +``` + +--- + +## Measure Graph Widget + +**GET** `/traction/api/v1/dashboard/widgets/measure-graph/data` + +Get measure data with historical values and trend. + +### Query Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `instanceId` | string | Yes | Widget instance ID. Settings are loaded by this id. | + +### Response + +```json +{ + "success": true, + "data": { + "measureId": "measure_revenue", + "measureName": "Monthly Recurring Revenue", + "unit": "USD", + "direction": "up", + "currentValue": 95000, + "targetValue": 100000, + "variance": -5.0, + "dataPoints": [ + { "date": "2025-10-01", "value": 75000, "dataType": "actual" }, + { "date": "2025-10-01", "value": 80000, "dataType": "target" }, + { "date": "2025-11-01", "value": 82000, "dataType": "actual" }, + { "date": "2025-11-01", "value": 90000, "dataType": "target" }, + { "date": "2025-12-01", "value": 90000, "dataType": "actual" }, + { "date": "2025-12-01", "value": 95000, "dataType": "target" }, + { "date": "2026-01-01", "value": 95000, "dataType": "actual" }, + { "date": "2026-01-01", "value": 100000, "dataType": "target" } + ], + "trend": { + "direction": "up", + "changePercent": 26.7, + "projection": 102000 + } + } +} +``` + +**Note:** The `dataPoints` array includes all data points (both actual and target values) with an explicit `dataType` field indicating whether each point is an "actual" recorded value or a "target" planned value. This allows the frontend to distinguish between actual and target data points and supports scenarios where there may be targets without actuals or vice versa. + +--- + +## Actions By Status Widget + +**GET** `/traction/api/v1/dashboard/widgets/actions-by-status/data` + +Get action counts grouped by status, assignee, or priority. + +### Query Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `instanceId` | string | Yes | Widget instance ID. Settings are loaded by this id. | + +### Response + +```json +{ + "success": true, + "data": { + "groups": [ + { "name": "Not Started", "key": "not_started", "count": 12, "percentage": 24 }, + { "name": "In Progress", "key": "in_progress", "count": 25, "percentage": 50 }, + { "name": "Blocked", "key": "blocked", "count": 3, "percentage": 6 }, + { "name": "Completed", "key": "completed", "count": 10, "percentage": 20 } + ], + "total": 50 + } +} +``` + +--- + +## AI Insights Widget + +**GET** `/traction/api/v1/dashboard/widgets/ai-insights/data` + +Get AI-generated insights and recommendations with KISS framework categorization. + +### Query Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `instanceId` | string | Yes | Widget instance ID. Settings are loaded by this id. | + +### Response + +```json +{ + "success": true, + "data": { + "insights": [ + { + "id": "insight_001", + "title": "Focus on Enterprise Segment", + "description": "Analysis shows 40% higher conversion rates in enterprise segment. This represents a significant opportunity to improve revenue quality and customer lifetime value.", + "category": "strategy", + "priority": "high", + "kissCategory": "start", + "alignmentImpact": "This insight directly supports our growth objectives by targeting higher-value customer segments that align with our core competencies.", + "status": "active", + "createdAt": "2026-02-02T23:09:51.3271249Z", + "updatedAt": "2026-02-02T23:09:51.3271249Z", + "metadata": { + "conversationCount": 0, + "businessImpact": "high", + "effortRequired": "medium" + } + } + ], + "total": 1 + } +} +``` + +### Response Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `data.insights` | array | Yes | Array of insight objects | +| `data.total` | number | Yes | Total count of insights | +| `insights[].id` | string | Yes | Unique insight identifier | +| `insights[].title` | string | Yes | Short, actionable insight title | +| `insights[].description` | string | Yes | Detailed explanation and context | +| `insights[].category` | string | Yes | Insight category: "strategy" \| "operations" \| "finance" \| "marketing" \| "leadership" \| "technology" | +| `insights[].priority` | string | Yes | Priority level: "low" \| "medium" \| "high" \| "critical" | +| `insights[].kissCategory` | string | Yes | KISS framework: "keep" \| "improve" \| "start" \| "stop" | +| `insights[].alignmentImpact` | string | No | How this affects purpose/values alignment and business outcomes | +| `insights[].status` | string | Yes | Current status: "active" \| "dismissed" \| "acknowledged" \| "inProgress" \| "completed" | +| `insights[].createdAt` | string | Yes | ISO 8601 timestamp of creation | +| `insights[].updatedAt` | string | No | ISO 8601 timestamp of last update | +| `insights[].metadata.conversationCount` | number | No | Number of coaching conversations | +| `insights[].metadata.businessImpact` | string | No | Business impact: "low" \| "medium" \| "high" | +| `insights[].metadata.effortRequired` | string | No | Required effort: "low" \| "medium" \| "high" | + +### Notes + +- **IMPORTANT:** All fields use **camelCase** (backend v4.0 standard) +- **IMPORTANT:** Response format matches other dashboard widgets (object with array + count) +- `kissCategory` is **required** for proper widget display +- Widget groups insights by KISS category (Keep, Improve, Start, Stop) +- See [Coaching Service](./coaching-service.md) for insight generation +- See [Common Patterns](./common-patterns.md) v4.0 for camelCase standard +- See GitHub Issue #634 for implementation details + +--- + +## Performance Score Widget + +**GET** `/traction/api/v1/dashboard/widgets/performance-score/data` + +Get overall performance score with component breakdown. + +### Query Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `instanceId` | string | Yes | Widget instance ID. Settings are loaded by this id. | + +### Response + +```json +{ + "success": true, + "data": { + "overallScore": 78, + "previousScore": 72, + "change": 6, + "trend": "improving", + "components": [ + { "name": "Goal Completion", "key": "goalCompletion", "score": 85, "weight": 0.3 }, + { "name": "Measure Performance", "key": "measurePerformance", "score": 72, "weight": 0.25 }, + { "name": "Timeline Adherence", "key": "timelineAdherence", "score": 68, "weight": 0.25 }, + { "name": "Business Growth", "key": "businessGrowth", "score": 87, "weight": 0.2 } + ], + "history": [ + { "date": "2025-10-01", "score": 65 }, + { "date": "2025-11-01", "score": 68 }, + { "date": "2025-12-01", "score": 72 }, + { "date": "2026-01-01", "score": 78 } + ] + } +} +``` + +--- + +## Team Alignment Widget + +**GET** `/traction/api/v1/dashboard/widgets/team-alignment/data` + +Get team alignment score with factor breakdown. + +### Query Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `instanceId` | string | Yes | Widget instance ID. Settings are loaded by this id. | + +### Response + +```json +{ + "success": true, + "data": { + "alignmentScore": 82, + "previousScore": 78, + "change": 4, + "trend": "improving", + "factors": [ + { "name": "Values Alignment", "key": "valuesAlignment", "score": 88, "icon": "heart" }, + { "name": "Goal Distribution", "key": "goalDistribution", "score": 75, "icon": "target" }, + { "name": "Collaboration", "key": "collaboration", "score": 85, "icon": "users" }, + { "name": "Communication", "key": "communication", "score": 80, "icon": "message" } + ], + "history": [ + { "date": "2025-10-01", "score": 72 }, + { "date": "2025-11-01", "score": 75 }, + { "date": "2025-12-01", "score": 78 }, + { "date": "2026-01-01", "score": 82 } + ] + } +} +``` + +--- + +# Data Types Summary + +## Widget Categories + +| Category | Description | Example Widgets | +|----------|-------------|-----------------| +| `overview` | Summary/aggregation | Hot List, Performance Score | +| `actions` | Action management | Action List, Actions By Status | +| `goals` | Goal tracking | Goal Progress | +| `measures` | Metric visualization | Measure Graph | +| `issues` | Issue management | Issue List | +| `insights` | AI/coaching | AI Insights | +| `team` | Team analytics | Team Alignment | + +## Widget Access Control + +Widget accessibility is determined by the backend based on: +- User's current subscription tier (free, starter, professional, enterprise) +- Enabled feature flags for the user +- Dynamic business rules managed by the backend + +The widget catalog API returns `isAccessible: boolean` for each widget, eliminating the need for the frontend to evaluate tier requirements or feature flags. This allows the backend to manage tier definitions and access rules dynamically without requiring frontend code changes. + +**Note**: Tier and feature requirements are internal backend concerns and are not exposed in the API response. + +## Grid Configuration + +```typescript +const GRID_CONFIG = { + cols: { lg: 5, md: 4, sm: 3, xs: 2 }, + rowHeight: 120, // pixels + breakpoints: { lg: 1200, md: 996, sm: 768, xs: 0 }, + containerPadding: [16, 16], + margin: [16, 16], +}; +``` + +--- + +# Error Codes + +| Code | HTTP Status | Description | +|------|-------------|-------------| +| `VALIDATION_ERROR` | 400 | Invalid request parameters | +| `UNAUTHORIZED` | 401 | Missing or invalid authentication | +| `FORBIDDEN` | 403 | Widget/template not accessible for tier | +| `RESOURCE_NOT_FOUND` | 404 | Widget, template, or config not found | +| `INTERNAL_SERVER_ERROR` | 500 | Server error | + +--- + +# Related APIs + +- **[Dashboard, Reports & Activities API](./traction-service/dashboard-reports-activities-api.md)**: Command center data +- **[Actions API](./traction-service/actions-api.md)**: Action data source +- **[Goals API](./traction-service/goals-api.md)**: Goal data source +- **[Measures API](./traction-service/measures-api.md)**: Measure data source +- **[Issues API](./traction-service/issues-api.md)**: Issue data source +- **[Coaching Service](./coaching-service.md)**: AI insights data source diff --git a/docs/shared/Specifications/user-app/index.md b/docs/shared/Specifications/user-app/index.md new file mode 100644 index 00000000..8b2ae888 --- /dev/null +++ b/docs/shared/Specifications/user-app/index.md @@ -0,0 +1,289 @@ +# PurposePath Backend Integration Specifications - Master Index + +**Document Date:** December 23, 2025 +**Version:** 7.0 +**Purpose:** Source of truth for frontend-backend integration points + +## Document Structure + +This specification is split into multiple documents for efficient AI assistant consumption. Each document can be referenced independently or as a complete set. + +### Core Service Specifications + +1. **[User & Tenant Management Service](./user-tenant-service.md)** ⭐ NEW (v1.0) + - Authentication (Login, Register, Google OAuth, Password Reset) + - User Profile Management + - Email Verification + - Subscription & Billing Integration + - User Features & Limits + +2. **[Account API (Auth/Billing/Subscriptions)](./account-api.md)** ⭐ Consolidated (v2.0) + - Auth flows, profile, tenant endpoints + - Subscription tiers, user subscriptions + - Billing portal, payment intents, provider webhooks + - Health endpoints + +3. **[Business Foundation Service](./business-foundation-service.md)** ⭐ NEW (v1.0) + - Business Profile, Identity, Market, Proposition, Model + - Core Values Management + - Ideal Customer Avatars (ICAs) + - Products & Services Inventory + - Wizard Progress Tracking + +4. **[People Service](./people-service.md)** + - Person CRUD operations + - Person tags and types + - User-person relationships + +5. **[Org Structure Service](./org-structure-service.md)** + - Organization roles and permissions + - Organization chart and relationships + - User org structure endpoints + +6. **[AI/Coaching Service](../ai-user/backend-integration-unified-ai.md)** + - AI/ML Endpoints (Alignment, Validation, Suggestions) + - Business Insights & Metrics + - Coaching Conversations + - Strategic Planning AI + +7. **[Traction Service](./traction-service/README.md)** ⭐ MODULAR STRUCTURE (v7) + - **Controller-based specifications** for easier maintenance + - Goals, Measures, Measure Links, Measure Data, Actions, Issues, People, Dashboard + - [View Traction Service Index →](./traction-service/README.md) + +8. **[Dashboard Service](./dashboard-service.md)** ⭐ NEW (v1.0) + - User Dashboard Configuration CRUD + - System Templates Management + - Widget Catalog & Registry + - Dynamic Widget Data Retrieval + - Responsive Grid Layouts + +9. **[Common Patterns & Data Models](./common-patterns.md)** + - Authentication Headers + - Error Handling + - Data Models & Enumerations + - Environment Configuration + +--- + +## 🎉 What's New in v1.0 (December 30, 2025) + +### Account Service Split & Cleanup +- **Extracted Account Service into two focused documents:** + - User & Tenant Management - Authentication, user profile, subscriptions, billing + - Business Foundation - Foundation sections, wizard, core values, ICAs, products +- **Removed deprecated endpoints:** Onboarding endpoints (moved to Business Foundation), Admin discount codes (not user-frontend), People/Org structure endpoints (out of scope) +- **Standardized naming:** All property names now use camelCase (not snake_case) to match frontend implementation +- **Specification now reflects implementation:** Documents describe actual working code, not aspirational features + +### Documentation Standards +- ✅ All endpoints with actual implementation reference +- ✅ Field-by-field request/response examples +- ✅ Frontend service file locations documented +- ✅ Error handling patterns explained +- ✅ CamelCase property names throughout + +**Total User/Tenant Endpoints:** 16 +**Total Business Foundation Endpoints:** 25 +**Total Documented:** 41 endpoints across 2 new service specs + +## Architecture Overview + +### Microservices Structure + +PurposePath frontend integrates with three backend microservices through RESTful APIs and Server-Sent Events (SSE). + +``` +┌─────────────────┐ +│ Frontend │ +│ (React/TS) │ +└────────┬────────┘ + │ + ┌────┴────┐ + │ axios │ + └────┬────┘ + │ + ┌────┴────────────────────────────────┐ + │ │ +┌───▼────────┐ ┌──────────┐ ┌─────────▼────┐ +│ Account │ │ Coaching │ │ Traction │ +│ Service │ │ Service │ │ Service │ +└────────────┘ └──────────┘ └──────────────┘ + Auth/Users AI/ML/Insights Goals/Ops/Measures + Billing Real-time +``` + +### Service Base URLs + +| Service | Environment Variable | Default (Localhost) | +|---------|---------------------|---------------------| +| Account | `REACT_APP_ACCOUNT_API_URL` | `http://localhost:8001` | +| Coaching | `REACT_APP_COACHING_API_URL` | `http://localhost:8000` | +| Traction | `REACT_APP_TRACTION_API_URL` | `http://localhost:8002` | + +### Service Responsibilities + +#### Account Service (User & Tenant Management) + +- User authentication (email/password, Google OAuth) +- User profile management +- Email verification and password reset +- Subscription tiers and billing +- Payment processing (Stripe integration) +- User feature access and usage limits +- Token refresh and session management + +#### Account Service (Business Foundation) + +- Business profile (name, website, address, industry) +- Core identity (vision, purpose, core values) +- Target market (niche, key problems, ideal customer avatars) +- Value proposition (offer, audience, differentiator) +- Business model (revenue, pricing, partnerships) +- Products and services inventory +- Multi-step wizard progress tracking + +#### People Service + +- Person CRUD operations +- Person attributes and tags +- Person type definitions +- User-person relationships + +#### Org Structure Service + +- Organization roles and permissions +- Org chart management +- Reporting relationships +- Access control structures + +#### Coaching Service + +- All AI/ML operations +- Goal alignment calculations +- Strategy suggestions +- Measure recommendations +- Business insights generation +- Root cause analysis +- Action prioritization and scheduling +- Coaching conversations + +#### Traction Service + +- Goals CRUD operations +- Strategies and Measures management +- Operations (Actions and Issues) +- Goal-Strategy-Measure relationships +- Activity feeds +- Company reports +- Real-time updates via SSE + +## Implementation Details + +### Frontend Service Files + +| Service | Primary Client | Implementation Files | +|---------|----------------|---------------------| +| User & Tenant Mgmt | `api.ts` → `accountClient` | `src/services/api.ts` | +| Business Foundation | `business-foundation-service.ts` → `accountClient` | `src/services/business-foundation-service.ts`, `src/types/business-foundation.ts` | +| People | TBD | TBD | +| Org Structure | TBD | TBD | +| Coaching | `api.ts` → `coachingClient` | `src/services/api.ts`, `src/services/alignment-engine-service.ts`, `src/services/strategy-suggestion-service.ts`, `src/services/measure-recommendation-service.ts`, `src/services/operations-ai-service.ts` | +| Traction | `traction.ts` → `traction` | `src/services/traction.ts`, `src/services/goal-service.ts`, `src/services/action-service.ts`, `src/services/issue-service.ts`, `src/services/operations-traction-service.ts`, `src/services/measure-planning-service.ts`, `src/services/realtime.ts` | + +### Authentication Flow + +1. User logs in → POST /auth/login (Account Service) +2. Receives: accessToken, refreshToken, user, tenant +3. Stores: localStorage.accessToken, localStorage.refreshToken, localStorage.tenantId +4. All subsequent requests include: + - Authorization: Bearer {accessToken} + - X-Tenant-Id: {tenantId} +5. On 401 response → POST /auth/refresh (Account Service) +6. Updates tokens and retries original request + +### Request Interceptors + +All service clients (accountClient, coachingClient, traction) implement: + +- **Token injection**: Automatically adds `Authorization: Bearer {token}` header +- **Tenant header**: Automatically adds `X-Tenant-Id` header +- **Token refresh**: On 401 response, attempts token refresh and retries request +- **Special headers**: Adds `X-Frontend-Base-Url` for email-triggering endpoints + +## Quick Reference + +### Most Common Endpoints + +| Operation | Endpoint | Service | +|-----------|----------|---------| +| Login | `POST /auth/login` | Account | +| Get User Profile | `GET /user/profile` | Account | +| Get User Subscription | `GET /user/subscription` | Account | +| List Goals | `GET /goals` | Traction | +| Create Goal | `POST /goals` | Traction | +| Calculate Alignment | `POST /api/coaching/alignment-check` | Coaching | +| Get Strategy Suggestions | `POST /api/coaching/strategy-suggestions` | Coaching | +| List Actions | `GET /api/operations/actions` | Traction | +| Create Action | `POST /api/operations/actions` | Traction | +| Real-time Goal Updates | `GET /realtime/goals/{goalId}/activity` (SSE) | Traction | + +### Common HTTP Status Codes + +| Code | Meaning | Frontend Action | +|------|---------|----------------| +| 200 | Success | Process response data | +| 401 | Unauthorized | Trigger token refresh, retry | +| 403 | Forbidden | Show access denied message | +| 404 | Not Found | Handle missing resource | +| 422 | Validation Error | Show field-specific errors | +| 500 | Server Error | Show generic error, enable retry | + +### Environment Variables + +```bash +# Service URLs +REACT_APP_ACCOUNT_API_URL=https://api.dev.purposepath.app/account/api/v1 +REACT_APP_COACHING_API_URL=https://api.dev.purposepath.app/coaching/api/v1 +REACT_APP_TRACTION_API_URL=https://api.dev.purposepath.app/traction/api/v1 + +# Feature Flags +REACT_APP_FEATURE_REALTIME=true + +# SSE Configuration +REACT_APP_SSE_BASE_URL=https://api.dev.purposepath.app/traction/api/v1 + +# Optional Features +REACT_APP_FE_BASE_HEADER_LOGIN=false +``` + +## Version History + +### Version 3.0 (October 13, 2025) + +- Split into multiple documents for AI assistant efficiency +- Updated to reflect current implementation state +- Added frontend service file mappings +- Clarified service responsibilities +- Added implementation details and architecture diagrams + +### Version 2.1 (October 1, 2025) + +- Added AI/ML endpoints to Coaching Service +- Clarified alignment engine routing +- Added operations AI endpoints + +### Version 2.0 (Previous) + +- Multi-service architecture +- Separated Account, Coaching, and Traction concerns + +--- + +**Navigation:** + +- [Account Service Specs →](./account-service.md) +- [AI/Coaching Service Specs →](../ai-user/backend-integration-unified-ai.md) +- [Traction Service Specs →](./traction-service/README.md) +- [People & Org Structure →](./people-service.md) +- [Common Patterns & Data Models →](./common-patterns.md) diff --git a/docs/shared/Specifications/user-app/org-structure-service.md b/docs/shared/Specifications/user-app/org-structure-service.md new file mode 100644 index 00000000..3727acdf --- /dev/null +++ b/docs/shared/Specifications/user-app/org-structure-service.md @@ -0,0 +1,3023 @@ +# Organizational Structure Service - API Specification + +**Version:** 3.1 +**Last Updated:** January 4, 2026 +**Service Base URL:** `{REACT_APP_ACCOUNT_API_URL}` +**Default (Localhost):** `http://localhost:8001` + +[← People Service](./people-service.md) | [Back to Index](./index.md) + +> **📋 NOTE:** Admin Template endpoints have been moved to the [Admin Portal API Specification](../admin-portal/admin-api-specification.md#role-templates). + +--- + +## Overview + +The Organizational Structure module manages the organizational hierarchy through roles, positions, organization units, and their relationships. It provides the foundation for the accountability framework within PurposePath. + +### Architecture Overview + +The org structure is built on these key entities: + +``` +┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ +│ RoleType │ │ Role │ │OrganizationUnit- │ +│ (Reference) │────▶│ (Template) │ │ Type │ +│ │ │ │ │ (Reference) │ +└──────────────────┘ └────────┬─────────┘ └────────┬─────────┘ + │ │ + │ ▼ + │ ┌──────────────────┐ + │ │ OrganizationUnit │ + │ │ (Department/ │ + │ │ Team/etc.) │ + │ └────────┬─────────┘ + │ │ + ▼ │ + ┌──────────────────┐ │ + │ Position │◀─────────────┘ + │ (Instance of Role│ + │ in an Org Unit) │ + └────────┬─────────┘ + │ + ▼ + ┌──────────────────┐ + │ Person │ + │ (Occupant) │ + └──────────────────┘ +``` + +### Key Concepts + +- **Role**: A template defining responsibilities and accountability (e.g., "Software Engineer", "VP Sales") +- **RoleType**: Classification of roles (Executive, Management, Professional, Associate) +- **Position**: An instance of a role within an organization unit, optionally filled by a person +- **OrganizationUnit**: A logical grouping (Company, Division, Department, Team) +- **OrganizationUnitType**: Classification of organization units +- **Position Relationship**: Non-hierarchical connection between positions (Support, Advise, Collaborate, Mentor) +- **Reports To**: Hierarchical relationship defined on Position entity + +### Frontend Implementation + +- **Primary Client:** `accountClient` (axios instance) +- **Related Files:** + - `src/services/roles-service.ts` - Role CRUD operations + - `src/services/positions-service.ts` - Position management *(planned)* + - `src/services/org-units-service.ts` - Organization unit management *(planned)* + - `src/services/role-relationships-service.ts` - Reporting/collaboration structures + - `src/services/org-chart-service.ts` - Organization chart visualization + +--- + +## Roles Endpoints + +### GET /roles + +List all roles with filtering and pagination. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| status | string | `active` | Filter: `active`, `inactive`, `all` | +| vacant | boolean | - | Filter for vacant roles only | +| search | string | - | Search by name or code | +| page | number | 1 | Page number (1-based) | +| pageSize | number | 20 | Items per page (max 100) | +| sortBy | string | `name` | Sort field: `name`, `code`, `createdAt` | +| sortOrder | string | `asc` | Sort order: `asc`, `desc` | + +**Response:** + +```json +{ + "success": true, + "data": { + "items": [ + { + "id": "string (GUID)", + "code": "string", + "name": "string", + "description": "string?", + "roleTypeId": "string (GUID)", + "roleType": { + "id": "string (GUID)", + "code": "string", + "name": "string" + }, + "isActive": "boolean", + "currentOccupant": { + "id": "string (GUID)?", + "name": "string?", + "since": "string (ISO 8601)?" + }, + "reportsTo": { + "roleId": "string (GUID)?", + "roleName": "string?" + }, + "directReportsCount": "number", + "createdAt": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)?" + } + ], + "pagination": { + "page": "number", + "pageSize": "number", + "totalItems": "number", + "totalPages": "number" + } + } +} +``` + +--- + +### GET /roles/dropdown + +Get simplified role list for dropdowns. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| includeVacant | boolean | true | Include vacant roles | +| excludeId | string (GUID) | - | Exclude specific role (for relationship forms) | + +**Response:** + +```json +{ + "success": true, + "data": [ + { + "id": "string (GUID)", + "code": "string", + "name": "string", + "occupantName": "string?", + "isVacant": "boolean" + } + ] +} +``` + +--- + +### GET /roles/{id} + +Get detailed role information. + +**Path Parameters:** + +- `id` - Role ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "code": "string", + "name": "string", + "accountability": "string", + "description": "string?", + "roleTypeId": "string (GUID)", + "roleType": { + "id": "string (GUID)", + "code": "string", + "name": "string", + "isStretchRole": "boolean" + }, + "isActive": "boolean", + "currentOccupant": { + "id": "string (GUID)?", + "firstName": "string?", + "lastName": "string?", + "email": "string?", + "title": "string?", + "since": "string (ISO 8601)?", + "isPrimaryRole": "boolean?" + }, + "assignmentHistory": [ + { + "personId": "string (GUID)", + "personName": "string", + "effectiveDate": "string (ISO 8601)", + "terminationDate": "string (ISO 8601)?" + } + ], + "reportsTo": { + "roleId": "string (GUID)?", + "roleCode": "string?", + "roleName": "string?", + "occupantName": "string?" + }, + "directReports": [ + { + "roleId": "string (GUID)", + "roleCode": "string", + "roleName": "string", + "occupantName": "string?" + } + ], + "relationships": [ + { + "relationshipId": "string (GUID)", + "direction": "from | to", + "roleId": "string (GUID)", + "roleCode": "string", + "roleName": "string", + "occupantName": "string?", + "relationshipType": { + "code": "string", + "name": "string", + "verb": "string" + }, + "description": "string?" + } + ], + "createdAt": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)?", + "createdBy": "string (GUID)", + "updatedBy": "string (GUID)?" + } +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Role not found", + "code": "RESOURCE_NOT_FOUND", + "details": { + "resourceType": "Role", + "resourceId": "guid" + } +} +``` + +--- + +### POST /roles + +Create a new role. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Request:** + +```json +{ + "code": "string", + "name": "string", + "accountability": "string", + "description": "string?", + "roleTypeId": "string (GUID)?", + "reportsToRoleId": "string (GUID)?" +} +``` + +**Field Constraints:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| code | string | Yes | 2-20 chars, uppercase alphanumeric + underscore, unique within tenant | +| name | string | Yes | 1-100 characters | +| accountability | string | Yes | 1-500 characters, describes what this role is accountable for | +| description | string | No | Max 2000 characters (detailed responsibilities, can include markdown) | +| roleTypeId | GUID | No | Must be valid RoleType ID. Defaults to "Professional" type if not specified. | +| reportsToRoleId | GUID | No | Must be valid active role | + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "code": "string", + "name": "string", + "accountability": "string", + "description": "string?", + "roleTypeId": "string (GUID)", + "roleType": { + "id": "string (GUID)", + "code": "string", + "name": "string" + }, + "isActive": true, + "currentOccupant": null, + "reportsTo": null, + "directReports": [], + "createdAt": "string (ISO 8601)", + "createdBy": "string (GUID)" + } +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Role code already exists", + "code": "DUPLICATE_RESOURCE", + "details": { + "field": "code" + } +} +``` + +--- + +### PUT /roles/{id} + +Update an existing role. + +**Path Parameters:** + +- `id` - Role ID (GUID) + +**Request:** + +```json +{ + "name": "string?", + "accountability": "string?", + "description": "string?", + "reportsToRoleId": "string (GUID)?" +} +``` + +**Field Constraints:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| name | string | No | 1-100 characters | +| accountability | string | No | 1-500 characters, describes what this role is accountable for | +| description | string | No | Max 2000 characters (detailed responsibilities) | +| reportsToRoleId | GUID | No | Must be active role, cannot create circular reference, set to `null` to remove | + +**Notes:** + +- `code` cannot be changed after creation +- Setting `reportsToRoleId` to `null` removes the reporting relationship (makes role a top-level role) +- Backend validates no circular references when changing reportsTo +- Non-hierarchical relationships (SUPPORT, ADVISE, etc.) managed via separate endpoints + +**Response:** + +```json +{ + "success": true, + "data": "RoleResponse (full object)" +} +``` + +--- + +### DELETE /roles/{id} + +Soft delete a role. + +**Path Parameters:** + +- `id` - Role ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Cannot delete role with active assignment", + "code": "BUSINESS_RULE_VIOLATION", + "details": { + "occupantId": "guid", + "occupantName": "John Smith" + } +} +``` + +```json +{ + "success": false, + "error": "Cannot delete role with direct reports", + "code": "BUSINESS_RULE_VIOLATION", + "details": { + "directReportsCount": 5 + } +} +``` + +--- + +### POST /roles/{id}/activate + +Reactivate a deactivated role. + +**Path Parameters:** + +- `id` - Role ID (GUID) + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "isActive": true + } +} +``` + +--- + +### POST /roles/{id}/deactivate + +Deactivate a role (unassigns person, removes from hierarchy). + +**Path Parameters:** + +- `id` - Role ID (GUID) + +**Request (Optional):** + +```json +{ + "cascadeDirectReports": "boolean?", + "newParentRoleId": "string (GUID)?" +} +``` + +**Field Constraints:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| cascadeDirectReports | boolean | No | If true, deactivate all direct reports | +| newParentRoleId | GUID | No | Reassign direct reports to this role | + +**Default Behavior (when no options provided):** + +- Direct reports' `reportsToRoleId` is set to `null` (they become top-level roles) +- The deactivated role's person assignment is terminated (if any) +- All relationships (SUPPORT, ADVISE, etc.) involving this role are removed + +**Response:** + +```json +{ + "success": true, + "data": { + "deactivated": true, + "personUnassigned": "boolean", + "relationshipsRemoved": "number", + "directReportsHandled": { + "reassignedTo": "string (GUID)?", + "deactivatedCount": "number?" + } + } +} +``` + +--- + +## Role Relationships Endpoints + +### GET /role-relationships + +List all role relationships. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| type | string | - | Filter by type: `reportsTo`, `collaboratesWith` | +| roleId | GUID | - | Get relationships for specific role | + +**Response:** + +```json +{ + "success": true, + "data": [ + { + "id": "string (GUID)", + "fromRole": { + "id": "string (GUID)", + "code": "string", + "name": "string", + "occupantName": "string?" + }, + "toRole": { + "id": "string (GUID)", + "code": "string", + "name": "string", + "occupantName": "string?" + }, + "relationshipType": { + "id": "string (GUID)", + "code": "string", + "name": "string" + }, + "description": "string?", + "createdAt": "string (ISO 8601)" + } + ] +} +``` + +--- + +### POST /role-relationships + +Create a role relationship. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Request:** + +```json +{ + "fromRoleId": "string (GUID)", + "toRoleId": "string (GUID)", + "relationshipTypeCode": "string", + "description": "string?" +} +``` + +**Field Constraints:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| fromRoleId | GUID | Yes | Must be active role | +| toRoleId | GUID | Yes | Must be active role, different from fromRoleId | +| relationshipTypeCode | string | Yes | Valid relationship type code (e.g., `SUPPORT`, `ADVISE`, `COLLABORATE`, `MENTOR`) | +| description | string | No | Max 500 chars | + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "fromRole": { + "id": "string (GUID)", + "code": "string", + "name": "string" + }, + "toRole": { + "id": "string (GUID)", + "code": "string", + "name": "string" + }, + "relationshipType": { + "id": "string (GUID)", + "code": "string", + "name": "string" + }, + "description": "string?", + "createdAt": "string (ISO 8601)" + } +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Relationship already exists between these roles with this type", + "code": "DUPLICATE_RESOURCE", + "details": { + "existingRelationshipId": "guid" + } +} +``` + +```json +{ + "success": false, + "error": "Relationship type does not allow multiple relationships", + "code": "BUSINESS_RULE_VIOLATION", + "details": { + "relationshipTypeCode": "MENTOR", + "existingRelationshipId": "guid" + } +} +``` + +--- + +### PUT /role-relationships/{id} + +Update a relationship (description only). + +**Path Parameters:** + +- `id` - Relationship ID (GUID) + +**Request:** + +```json +{ + "description": "string?" +} +``` + +**Response:** + +```json +{ + "success": true, + "data": "RoleRelationshipResponse" +} +``` + +--- + +### DELETE /role-relationships/{id} + +Delete a role relationship. + +**Path Parameters:** + +- `id` - Relationship ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true +} +``` + +--- + +### GET /role-relationship-types + +List available relationship types. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| includeInactive | boolean | false | Include inactive types | + +**Response:** + +```json +{ + "success": true, + "data": [ + { + "id": "string (GUID)", + "code": "SUPPORT", + "name": "Support", + "forwardVerb": "supports", + "reverseVerb": "is supported by", + "allowsMultiple": true, + "isActive": true, + "isSystem": true + }, + { + "id": "string (GUID)", + "code": "ADVISE", + "name": "Advise", + "forwardVerb": "advises", + "reverseVerb": "is advised by", + "allowsMultiple": true, + "isActive": true, + "isSystem": true + }, + { + "id": "string (GUID)", + "code": "COLLABORATE", + "name": "Collaborate", + "forwardVerb": "collaborates with", + "reverseVerb": "collaborates with", + "allowsMultiple": true, + "isActive": true, + "isSystem": true + }, + { + "id": "string (GUID)", + "code": "MENTOR", + "name": "Mentor", + "forwardVerb": "mentors", + "reverseVerb": "is mentored by", + "allowsMultiple": true, + "isActive": true, + "isSystem": true + } + ] +} +``` + +**Notes:** + +- `isSystem` indicates seeded types that cannot be deleted +- `forwardVerb` used when displaying "Role A {forwardVerb} Role B" +- `reverseVerb` used when displaying "Role B {reverseVerb} Role A" + +--- + +### GET /role-relationship-types/{id} + +Get relationship type details. + +**Path Parameters:** + +- `id` - RelationshipType ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "code": "string", + "name": "string", + "forwardVerb": "string", + "reverseVerb": "string", + "allowsMultiple": "boolean", + "isActive": "boolean", + "isSystem": "boolean", + "usageCount": "number", + "createdAt": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)?" + } +} +``` + +--- + +### POST /role-relationship-types + +Create a custom relationship type. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Request:** + +```json +{ + "code": "string", + "name": "string", + "forwardVerb": "string", + "reverseVerb": "string", + "allowsMultiple": "boolean?" +} +``` + +**Field Constraints:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| code | string | Yes | 2-20 chars, uppercase alphanumeric + underscore, unique within tenant | +| name | string | Yes | 1-50 characters | +| forwardVerb | string | Yes | 1-50 characters (e.g., "manages") | +| reverseVerb | string | Yes | 1-50 characters (e.g., "is managed by") | +| allowsMultiple | boolean | No | Default: true | + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "code": "string", + "name": "string", + "forwardVerb": "string", + "reverseVerb": "string", + "allowsMultiple": "boolean", + "isActive": true, + "isSystem": false, + "createdAt": "string (ISO 8601)" + } +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Relationship type code already exists", + "code": "DUPLICATE_RESOURCE", + "details": { + "field": "code" + } +} +``` + +--- + +### PUT /role-relationship-types/{id} + +Update a relationship type. + +**Path Parameters:** + +- `id` - RelationshipType ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Request:** + +```json +{ + "name": "string?", + "forwardVerb": "string?", + "reverseVerb": "string?", + "allowsMultiple": "boolean?" +} +``` + +**Notes:** + +- `code` cannot be changed after creation +- System types (`isSystem = true`) can have name/verbs updated but not deleted + +**Response:** + +```json +{ + "success": true, + "data": "RelationshipTypeResponse" +} +``` + +--- + +### DELETE /role-relationship-types/{id} + +Deactivate a relationship type. + +**Path Parameters:** + +- `id` - RelationshipType ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Cannot delete system relationship type", + "code": "BUSINESS_RULE_VIOLATION", + "details": { + "isSystem": true + } +} +``` + +```json +{ + "success": false, + "error": "Cannot delete relationship type with existing relationships", + "code": "BUSINESS_RULE_VIOLATION", + "details": { + "usageCount": 5 + } +} +``` + +--- + +## Role Types Endpoints + +Role Types provide classification categories for roles (Executive, Management, Professional, Associate). + +### GET /role-types + +List available role types. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| includeInactive | boolean | false | Include inactive types | + +**Response:** + +```json +{ + "success": true, + "data": [ + { + "id": "string (GUID)", + "code": "EXECUTIVE", + "name": "Executive", + "description": "C-level and senior leadership roles with strategic decision-making authority", + "isStretchRole": false, + "displayOrder": 1, + "isActive": true, + "isSystem": true + }, + { + "id": "string (GUID)", + "code": "MANAGEMENT", + "name": "Management", + "description": "Directors, VPs, and managers with team leadership responsibilities", + "isStretchRole": false, + "displayOrder": 2, + "isActive": true, + "isSystem": true + }, + { + "id": "string (GUID)", + "code": "PROFESSIONAL", + "name": "Professional", + "description": "Individual contributors with specialized expertise", + "isStretchRole": false, + "displayOrder": 3, + "isActive": true, + "isSystem": true + }, + { + "id": "string (GUID)", + "code": "ASSOCIATE", + "name": "Associate", + "description": "Entry-level and support roles", + "isStretchRole": false, + "displayOrder": 4, + "isActive": true, + "isSystem": true + } + ] +} +``` + +**Notes:** + +- `isSystem` indicates seeded types that cannot be deleted +- `isStretchRole` indicates extended responsibility positions (not full-time) +- `displayOrder` is used for sorting in UI dropdowns + +--- + +### GET /role-types/{id} + +Get role type details. + +**Path Parameters:** + +- `id` - RoleType ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "code": "string", + "name": "string", + "description": "string?", + "isStretchRole": "boolean", + "displayOrder": "number", + "isActive": "boolean", + "isSystem": "boolean", + "usageCount": "number", + "createdAt": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)?" + } +} +``` + +--- + +### POST /role-types + +Create a custom role type. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Request:** + +```json +{ + "code": "string", + "name": "string", + "description": "string?", + "isStretchRole": "boolean?", + "displayOrder": "number?" +} +``` + +**Field Constraints:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| code | string | Yes | 2-20 chars, uppercase alphanumeric + underscore, unique within tenant | +| name | string | Yes | 1-50 characters | +| description | string | No | Max 500 characters | +| isStretchRole | boolean | No | Default: false | +| displayOrder | number | No | Auto-assigned if not provided | + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "code": "string", + "name": "string", + "description": "string?", + "isStretchRole": "boolean", + "displayOrder": "number", + "isActive": true, + "isSystem": false, + "createdAt": "string (ISO 8601)" + } +} +``` + +--- + +### PUT /role-types/{id} + +Update a role type. + +**Path Parameters:** + +- `id` - RoleType ID (GUID) + +**Request:** + +```json +{ + "name": "string?", + "description": "string?", + "isStretchRole": "boolean?", + "displayOrder": "number?" +} +``` + +**Notes:** + +- `code` cannot be changed after creation +- System types (`isSystem = true`) can have name/description updated but not deleted + +--- + +### DELETE /role-types/{id} + +Deactivate a role type. + +**Path Parameters:** + +- `id` - RoleType ID (GUID) + +**Error Responses:** + +```json +{ + "success": false, + "error": "Cannot delete system role type", + "code": "BUSINESS_RULE_VIOLATION" +} +``` + +```json +{ + "success": false, + "error": "Cannot delete role type with existing roles", + "code": "BUSINESS_RULE_VIOLATION", + "details": { + "usageCount": 5 + } +} +``` + +--- + +## Organization Unit Types Endpoints + +Organization Unit Types classify organizational units (Company, Division, Department, Team, Project). + +### GET /organization-unit-types + +List available organization unit types. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| includeInactive | boolean | false | Include inactive types | + +**Response:** + +```json +{ + "success": true, + "data": [ + { + "id": "string (GUID)", + "code": "COMPANY", + "name": "Company", + "description": "Top-level organizational unit representing the entire company", + "displayOrder": 1, + "isActive": true, + "isSystem": true + }, + { + "id": "string (GUID)", + "code": "DIVISION", + "name": "Division", + "description": "Major business unit or division within the company", + "displayOrder": 2, + "isActive": true, + "isSystem": true + }, + { + "id": "string (GUID)", + "code": "DEPARTMENT", + "name": "Department", + "description": "Functional department within a division or company", + "displayOrder": 3, + "isActive": true, + "isSystem": true + }, + { + "id": "string (GUID)", + "code": "TEAM", + "name": "Team", + "description": "Work team or unit within a department", + "displayOrder": 4, + "isActive": true, + "isSystem": true + }, + { + "id": "string (GUID)", + "code": "PROJECT", + "name": "Project", + "description": "Cross-functional project team", + "displayOrder": 5, + "isActive": true, + "isSystem": true + } + ] +} +``` + +--- + +### GET /organization-unit-types/{id} + +Get organization unit type details. + +**Path Parameters:** + +- `id` - OrganizationUnitType ID (GUID) + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "code": "string", + "name": "string", + "description": "string?", + "displayOrder": "number", + "isActive": "boolean", + "isSystem": "boolean", + "usageCount": "number", + "createdAt": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)?" + } +} +``` + +--- + +### POST /organization-unit-types + +Create a custom organization unit type. + +**Request:** + +```json +{ + "code": "string", + "name": "string", + "description": "string?", + "displayOrder": "number?" +} +``` + +**Field Constraints:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| code | string | Yes | 2-20 chars, uppercase alphanumeric + underscore, unique within tenant | +| name | string | Yes | 1-50 characters | +| description | string | No | Max 500 characters | +| displayOrder | number | No | Auto-assigned if not provided | + +--- + +### PUT /organization-unit-types/{id} + +Update an organization unit type. + +**Request:** + +```json +{ + "name": "string?", + "description": "string?", + "displayOrder": "number?" +} +``` + +--- + +### DELETE /organization-unit-types/{id} + +Deactivate an organization unit type. + +**Error Responses:** + +```json +{ + "success": false, + "error": "Cannot delete system organization unit type", + "code": "BUSINESS_RULE_VIOLATION" +} +``` + +--- + +## Organization Units Endpoints + +Organization Units represent the logical structure of the organization (companies, divisions, departments, teams). + +### GET /organization-units + +List all organization units. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| parentId | GUID | - | Filter by parent unit (null for top-level) | +| typeId | GUID | - | Filter by organization unit type | +| includeInactive | boolean | false | Include inactive units | +| page | number | 1 | Page number | +| pageSize | number | 20 | Items per page | + +**Response:** + +```json +{ + "success": true, + "data": { + "items": [ + { + "id": "string (GUID)", + "name": "string", + "description": "string?", + "organizationUnitType": { + "id": "string (GUID)", + "code": "string", + "name": "string" + }, + "parentOrganizationUnitId": "string (GUID)?", + "parentOrganizationUnitName": "string?", + "unitLeadPersonId": "string (GUID)?", + "unitLeadPersonName": "string?", + "isActive": "boolean", + "childrenCount": "number", + "positionsCount": "number", + "createdAt": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)?" + } + ], + "pagination": { + "page": "number", + "pageSize": "number", + "totalItems": "number", + "totalPages": "number" + } + } +} +``` + +--- + +### GET /organization-units/tree + +Get hierarchical tree of organization units. + +**Response:** + +```json +{ + "success": true, + "data": { + "rootNodes": [ + { + "id": "string (GUID)", + "name": "string", + "organizationUnitType": { + "id": "string (GUID)", + "code": "string", + "name": "string" + }, + "unitLeadPersonName": "string?", + "positionsCount": "number", + "depth": "number", + "children": ["recursive OrgUnitNode"] + } + ] + } +} +``` + +--- + +### GET /organization-units/{id} + +Get organization unit details. + +**Path Parameters:** + +- `id` - OrganizationUnit ID (GUID) + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "name": "string", + "description": "string?", + "organizationUnitType": { + "id": "string (GUID)", + "code": "string", + "name": "string" + }, + "parentOrganizationUnit": { + "id": "string (GUID)?", + "name": "string?" + }, + "unitLeadPerson": { + "id": "string (GUID)?", + "displayName": "string?", + "email": "string?" + }, + "childUnits": [ + { + "id": "string (GUID)", + "name": "string", + "typeCode": "string" + } + ], + "positions": [ + { + "id": "string (GUID)", + "name": "string", + "roleName": "string", + "occupantName": "string?", + "isVacant": "boolean" + } + ], + "isActive": "boolean", + "createdAt": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)?" + } +} +``` + +--- + +### POST /organization-units + +Create an organization unit. + +**Request:** + +```json +{ + "name": "string", + "description": "string?", + "organizationUnitTypeId": "string (GUID)", + "parentOrganizationUnitId": "string (GUID)?", + "unitLeadPersonId": "string (GUID)?" +} +``` + +**Field Constraints:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| name | string | Yes | 1-100 characters | +| description | string | No | Max 500 characters | +| organizationUnitTypeId | GUID | Yes | Must be valid organization unit type | +| parentOrganizationUnitId | GUID | No | Must be valid active organization unit | +| unitLeadPersonId | GUID | No | Must be valid active person | + +--- + +### PUT /organization-units/{id} + +Update an organization unit. + +**Request:** + +```json +{ + "name": "string?", + "description": "string?", + "organizationUnitTypeId": "string (GUID)?", + "parentOrganizationUnitId": "string (GUID)?", + "unitLeadPersonId": "string (GUID)?" +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Cannot set parent to self or child unit (circular reference)", + "code": "CIRCULAR_REFERENCE" +} +``` + +--- + +### DELETE /organization-units/{id} + +Deactivate an organization unit. + +**Error Responses:** + +```json +{ + "success": false, + "error": "Cannot delete organization unit with child units", + "code": "BUSINESS_RULE_VIOLATION", + "details": { + "childUnitsCount": 3 + } +} +``` + +```json +{ + "success": false, + "error": "Cannot delete organization unit with active positions", + "code": "BUSINESS_RULE_VIOLATION", + "details": { + "positionsCount": 5 + } +} +``` + +--- + +### GET /organization-units/dropdown + +Get simplified organization unit list for dropdowns. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| typeId | GUID | - | Filter by organization unit type | + +**Response:** + +```json +{ + "success": true, + "data": [ + { + "id": "string (GUID)", + "name": "string", + "typeCode": "string", + "typeName": "string", + "parentId": "string (GUID)?", + "parentName": "string?", + "depth": "number" + } + ] +} +``` + +--- + +### PUT /organization-units/{id}/status + +Update organization unit status (activate/deactivate). + +**Path Parameters:** + +- `id` - OrganizationUnit ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Request:** + +```json +{ + "status": "active | inactive" +} +``` + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "name": "string", + "status": "active | inactive", + "updatedAt": "string (ISO 8601)" + } +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Cannot deactivate organization unit with active child units", + "code": "BUSINESS_RULE_VIOLATION" +} +``` + +--- + +## Positions Endpoints + +Positions represent instances of roles within organization units, optionally filled by persons. + +### GET /positions + +List all positions. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| status | string | `active` | Filter: `active`, `inactive`, `vacant`, `all` | +| organizationUnitId | GUID | - | Filter by organization unit | +| roleId | GUID | - | Filter by role | +| personId | GUID | - | Filter by assigned person | +| search | string | - | Search by name | +| page | number | 1 | Page number | +| pageSize | number | 20 | Items per page | + +**Response:** + +```json +{ + "success": true, + "data": { + "items": [ + { + "id": "string (GUID)", + "name": "string", + "description": "string?", + "specificAccountability": "string?", + "role": { + "id": "string (GUID)", + "code": "string", + "name": "string", + "roleTypeCode": "string" + }, + "organizationUnit": { + "id": "string (GUID)", + "name": "string", + "typeCode": "string" + }, + "person": { + "id": "string (GUID)?", + "displayName": "string?", + "email": "string?" + }, + "reportsToPosition": { + "id": "string (GUID)?", + "name": "string?", + "occupantName": "string?" + }, + "status": "active | inactive | vacant", + "directReportsCount": "number", + "createdAt": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)?" + } + ], + "pagination": { + "page": "number", + "pageSize": "number", + "totalItems": "number", + "totalPages": "number" + } + } +} +``` + +--- + +### GET /positions/{id} + +Get position details. + +**Path Parameters:** + +- `id` - Position ID (GUID) + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "name": "string", + "description": "string?", + "specificAccountability": "string?", + "role": { + "id": "string (GUID)", + "code": "string", + "name": "string", + "accountability": "string", + "roleType": { + "id": "string (GUID)", + "code": "string", + "name": "string" + } + }, + "organizationUnit": { + "id": "string (GUID)", + "name": "string", + "organizationUnitType": { + "id": "string (GUID)", + "code": "string", + "name": "string" + } + }, + "person": { + "id": "string (GUID)?", + "firstName": "string?", + "lastName": "string?", + "displayName": "string?", + "email": "string?", + "title": "string?" + }, + "reportsToPosition": { + "id": "string (GUID)?", + "name": "string?", + "roleName": "string?", + "occupantName": "string?" + }, + "directReports": [ + { + "id": "string (GUID)", + "name": "string", + "roleName": "string", + "occupantName": "string?", + "status": "string" + } + ], + "relationships": [ + { + "relationshipId": "string (GUID)", + "direction": "from | to", + "positionId": "string (GUID)", + "positionName": "string", + "occupantName": "string?", + "relationshipType": { + "code": "string", + "name": "string", + "verb": "string" + } + } + ], + "status": "active | inactive | vacant", + "createdAt": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)?" + } +} +``` + +--- + +### POST /positions + +Create a position. + +**Request:** + +```json +{ + "name": "string", + "description": "string?", + "specificAccountability": "string?", + "roleId": "string (GUID)", + "organizationUnitId": "string (GUID)", + "personId": "string (GUID)?", + "reportsToPositionId": "string (GUID)?" +} +``` + +**Field Constraints:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| name | string | Yes | 1-100 characters | +| description | string | No | Max 500 characters | +| specificAccountability | string | No | Max 500 characters (additional context beyond role accountability) | +| roleId | GUID | Yes | Must be valid active role | +| organizationUnitId | GUID | Yes | Must be valid active organization unit | +| personId | GUID | No | Must be valid active person | +| reportsToPositionId | GUID | No | Must be valid active position | + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "name": "string", + "status": "active | vacant", + "role": { "id": "string", "name": "string" }, + "organizationUnit": { "id": "string", "name": "string" }, + "person": { "id": "string?", "displayName": "string?" }, + "reportsToPosition": { "id": "string?", "name": "string?" }, + "createdAt": "string (ISO 8601)" + } +} +``` + +--- + +### PUT /positions/{id} + +Update a position. + +**Request:** + +```json +{ + "name": "string", + "roleId": "string (GUID)", + "organizationUnitId": "string (GUID)", + "description": "string?", + "specificAccountability": "string?", + "reportsToPositionId": "string (GUID)?" +} +``` + +**Notes:** + +- `roleId` may be changed via this endpoint +- To change the person, use the assign/unassign endpoints + +**Error Responses:** + +```json +{ + "success": false, + "error": "Cannot report to self", + "code": "BUSINESS_RULE_VIOLATION" +} +``` + +```json +{ + "success": false, + "error": "Would create circular reporting structure", + "code": "CIRCULAR_REFERENCE" +} +``` + +--- + +### DELETE /positions/{id} + +Deactivate a position. + +**Error Responses:** + +```json +{ + "success": false, + "error": "Cannot delete position with direct reports", + "code": "BUSINESS_RULE_VIOLATION", + "details": { + "directReportsCount": 3 + } +} +``` + +--- + +### GET /positions/dropdown + +Get simplified position list for dropdowns. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| organizationUnitId | GUID | - | Filter by organization unit | +| vacantOnly | boolean | false | Show only vacant positions | + +**Response:** + +```json +{ + "success": true, + "data": [ + { + "id": "string (GUID)", + "title": "string", + "roleTypeCode": "string", + "roleTypeName": "string", + "organizationUnitId": "string (GUID)?", + "organizationUnitName": "string?", + "status": "active | inactive | vacant", + "personId": "string (GUID)?", + "personName": "string?" + } + ] +} +``` + +--- + +### GET /positions/tree + +Get hierarchical position tree. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| rootPositionId | GUID | - | Start tree from specific position | +| organizationUnitId | GUID | - | Filter by organization unit | +| includeInactive | boolean | false | Include inactive positions | + +**Response:** + +```json +{ + "success": true, + "data": [ + { + "id": "string (GUID)", + "title": "string", + "roleCode": "string", + "roleName": "string", + "organizationUnitId": "string (GUID)?", + "organizationUnitName": "string?", + "status": "active | inactive | vacant", + "personId": "string (GUID)?", + "personName": "string?", + "reportsToId": "string (GUID)?", + "depth": "number", + "children": [/* recursive PositionTreeNode */] + } + ] +} +``` + +--- + +### POST /positions/{id}/assign + +Assign a person to a position. + +**Path Parameters:** + +- `id` - Position ID (GUID) + +**Request:** + +```json +{ + "personId": "string (GUID)" +} +``` + +**Response:** + +```json +{ + "success": true, + "data": { + "positionId": "string (GUID)", + "personId": "string (GUID)", + "personName": "string", + "status": "active", + "assignedAt": "string (ISO 8601)" + } +} +``` + +--- + +### POST /positions/{id}/unassign + +Remove person from a position. + +**Path Parameters:** + +- `id` - Position ID (GUID) + +**Response:** + +```json +{ + "success": true, + "data": { + "positionId": "string (GUID)", + "status": "vacant", + "unassignedAt": "string (ISO 8601)" + } +} +``` + +--- + +### PUT /positions/{id}/status + +Update position status. + +**Request:** + +```json +{ + "status": "active | inactive" +} +``` + +**Notes:** + +- Setting to `inactive` will also unassign any person +- `vacant` status is automatic when no person is assigned + +--- + +## Position Relationships Endpoints + +Position Relationships define non-hierarchical connections between positions (Support, Advise, Collaborate, Mentor). + +### GET /position-relationships + +List all position relationships. + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| positionId | GUID | - | Get relationships for specific position | +| typeCode | string | - | Filter by relationship type code | + +**Response:** + +```json +{ + "success": true, + "data": [ + { + "id": "string (GUID)", + "fromPosition": { + "id": "string (GUID)", + "name": "string", + "roleName": "string", + "occupantName": "string?" + }, + "toPosition": { + "id": "string (GUID)", + "name": "string", + "roleName": "string", + "occupantName": "string?" + }, + "relationshipType": { + "id": "string (GUID)", + "code": "string", + "name": "string", + "forwardVerb": "string", + "reverseVerb": "string" + }, + "description": "string?", + "createdAt": "string (ISO 8601)" + } + ] +} +``` + +--- + +### POST /position-relationships + +Create a position relationship. + +**Request:** + +```json +{ + "fromPositionId": "string (GUID)", + "toPositionId": "string (GUID)", + "relationshipTypeId": "string (GUID)", + "description": "string?" +} +``` + +**Field Constraints:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| fromPositionId | GUID | Yes | Must be active position | +| toPositionId | GUID | Yes | Must be active position, different from fromPositionId | +| relationshipTypeId | GUID | Yes | Must be valid RoleRelationshipType ID | +| description | string | No | Max 500 characters | + +--- + +### PUT /position-relationships/{id} + +Update a position relationship. + +**Request:** + +```json +{ + "description": "string?" +} +``` + +--- + +### DELETE /position-relationships/{id} + +Delete a position relationship. + +**Response:** + +```json +{ + "success": true +} +``` + +--- + +## Organization Chart Endpoints + +### GET /org-chart + +Get complete org chart tree structure. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| includeVacant | boolean | true | Include vacant positions | + +**Response:** + +```json +{ + "success": true, + "data": { + "rootNodes": [ + { + "role": { + "id": "string (GUID)", + "code": "string", + "name": "string", + "accountability": "string", + "description": "string?", + "isVacant": "boolean", + "isActive": "boolean" + }, + "assignedPersons": [ + { + "id": "string (GUID)", + "displayName": "string", + "title": "string?", + "email": "string?", + "isPrimary": "boolean", + "effectiveDate": "string (ISO 8601)" + } + ], + "children": [ + "recursive RoleNode" + ], + "relationships": null, + "depth": "number" + } + ], + "totalRoles": "number", + "totalPersons": "number", + "vacantRoles": "number", + "maxDepth": "number", + "generatedAt": "string (ISO 8601)" + } +} +``` + +**Notes:** + +- Top-level roles (no reportsTo) are roots of separate trees +- `assignedPersons` is an array to support multiple people assigned to one role +- Empty array when role is vacant + +--- + +### GET /org-chart/flat + +Get flat list for table/grid display. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| includeVacant | boolean | true | Include vacant positions | +| includeInactive | boolean | false | Include inactive roles | +| sortBy | string | `hierarchy` | `hierarchy`, `name`, `code` | + +**Response:** + +```json +{ + "success": true, + "data": [ + { + "roleId": "string (GUID)", + "roleCode": "string", + "roleName": "string", + "occupantId": "string (GUID)?", + "occupantName": "string?", + "reportsToRoleId": "string (GUID)?", + "reportsToRoleName": "string?", + "depth": "number", + "path": "string", + "isVacant": "boolean" + } + ] +} +``` + +**Notes:** + +- `path` contains the hierarchy path (e.g., "CEO → VP Sales → Sales Manager") +- When sorted by hierarchy, children appear after parents + +--- + +### GET /org-chart/role/{id}/subtree + +Get subtree under a specific role. + +**Path Parameters:** + +- `id` - Role ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| maxDepth | number | 10 | Maximum depth from role | +| includeVacant | boolean | true | Include vacant positions | + +**Response:** + +```json +{ + "success": true, + "data": { + "root": "RoleNode", + "totalDescendants": "number", + "vacantCount": "number" + } +} +``` + +--- + +### GET /org-chart/layouts/active + +Get the current user's active org chart layout (node positions). + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "data": { + "layoutId": "string (GUID)", + "layoutName": "string", + "isActive": true, + "positions": [ + { + "roleId": "string (GUID)", + "positionX": "number", + "positionY": "number" + } + ], + "createdAt": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)?" + } +} +``` + +**Status Codes:** + +- `200` - Success +- `404` - No active layout found + +--- + +### GET /org-chart/layouts + +Get all org chart layouts for the current user. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "data": { + "layouts": [ + { + "layoutId": "string (GUID)", + "layoutName": "string", + "isActive": "boolean", + "positionCount": "number", + "createdAt": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)?" + } + ] + } +} +``` + +--- + +### POST /org-chart/layouts + +Create a new layout or update an existing layout with node positions. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Request:** + +```json +{ + "layoutId": "string (GUID)?", + "layoutName": "string", + "positions": [ + { + "roleId": "string (GUID)", + "positionX": "number", + "positionY": "number" + } + ], + "setAsActive": "boolean" +} +``` + +**Field Constraints:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| layoutId | GUID | No | If provided, updates existing layout. Must belong to current user. | +| layoutName | string | Yes | 1-100 characters | +| positions | array | Yes | Array of node positions | +| positions[].roleId | GUID | Yes | Must be valid role in tenant | +| positions[].positionX | number | Yes | X coordinate | +| positions[].positionY | number | Yes | Y coordinate | +| setAsActive | boolean | No | Default: false. If true, sets as active layout after save. | + +**Response:** + +```json +{ + "success": true, + "data": { + "layoutId": "string (GUID)", + "layoutName": "string", + "isActive": "boolean", + "positions": [ + { + "roleId": "string (GUID)", + "positionX": "number", + "positionY": "number" + } + ], + "createdAt": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)?" + } +} +``` + +**Status Codes:** + +- `201` - Created (new layout) +- `200` - OK (updated existing layout) +- `400` - Validation error +- `404` - Layout not found (when updating with layoutId) +- `403` - Layout does not belong to user + +**Notes:** + +- If `layoutId` is null, creates new layout +- If `layoutId` provided, updates existing layout (must belong to current user) +- First layout for user is automatically activated regardless of `setAsActive` +- When `setAsActive` is true, deactivates other layouts for the user + +--- + +### PUT /org-chart/layouts/{layoutId}/activate + +Set a specific layout as the active layout for the current user. + +**Path Parameters:** + +- `layoutId` - Layout ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "data": { + "layoutId": "string (GUID)", + "layoutName": "string", + "isActive": true, + "positions": [ + { + "roleId": "string (GUID)", + "positionX": "number", + "positionY": "number" + } + ], + "createdAt": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)?" + } +} +``` + +**Status Codes:** + +- `200` - Success +- `404` - Layout not found +- `403` - Layout does not belong to user + +**Notes:** + +- Automatically deactivates any currently active layout for the user +- If layout is already active, returns success without changes + +--- + +### DELETE /org-chart/layouts/{layoutId} + +Delete a layout. Cannot delete the active layout. + +**Path Parameters:** + +- `layoutId` - Layout ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "message": "Layout deleted successfully" +} +``` + +**Status Codes:** + +- `200` - Success +- `400` - Cannot delete active layout +- `404` - Layout not found +- `403` - Layout does not belong to user + +**Notes:** + +- Cannot delete a layout that is currently active +- To delete active layout, activate a different layout first + +--- + +## Error Codes Reference + +| Code | HTTP Status | Description | +|------|-------------|-------------| +| `VALIDATION_ERROR` | 400 | Input validation failed | +| `UNAUTHORIZED` | 401 | Missing/invalid token | +| `FORBIDDEN` | 403 | Insufficient permissions | +| `RESOURCE_NOT_FOUND` | 404 | Resource does not exist | +| `DUPLICATE_RESOURCE` | 409 | Resource already exists (unique constraint) | +| `BUSINESS_RULE_VIOLATION` | 400 | Domain rule prevents operation | +| `CIRCULAR_REFERENCE` | 400 | Would create circular hierarchy (reports_to only) | +| `ROLE_HAS_ASSIGNMENT` | 400 | Role has active person assignment | +| `ROLE_HAS_DIRECT_REPORTS` | 400 | Role has subordinate roles | + +--- + +## Data Types Reference + +### RoleStatus + +```typescript +type RoleStatus = 'active' | 'inactive'; +``` + +### PositionStatus + +```typescript +type PositionStatus = 'active' | 'inactive' | 'vacant'; +``` + +### RoleType + +```typescript +interface RoleType { + id: string; + code: string; + name: string; + description: string | null; + isStretchRole: boolean; // Indicates extended responsibility, not full-time + displayOrder: number; + isActive: boolean; + isSystem: boolean; // System types cannot be deleted +} +``` + +### OrganizationUnitType + +```typescript +interface OrganizationUnitType { + id: string; + code: string; + name: string; + description: string | null; + displayOrder: number; + isActive: boolean; + isSystem: boolean; // System types cannot be deleted +} +``` + +### OrganizationUnit + +```typescript +interface OrganizationUnit { + id: string; + name: string; + description: string | null; + organizationUnitType: OrganizationUnitType; + parentOrganizationUnitId: string | null; + unitLeadPersonId: string | null; + isActive: boolean; +} +``` + +### Position + +```typescript +interface Position { + id: string; + name: string; + description: string | null; + specificAccountability: string | null; // Additional context beyond role + roleId: string; + organizationUnitId: string; + personId: string | null; + reportsToPositionId: string | null; + status: PositionStatus; +} +``` + +### PositionRelationship + +```typescript +interface PositionRelationship { + id: string; + fromPositionId: string; + toPositionId: string; + relationshipTypeId: string; // Uses RoleRelationshipType + description: string | null; + createdAt: string; +} +``` + +### RelationshipTypeCode + +```typescript +// Seeded system types (cannot be deleted) +type SystemRelationshipTypeCode = 'SUPPORT' | 'ADVISE' | 'COLLABORATE' | 'MENTOR'; + +// Custom types are user-defined strings following code constraints +type RelationshipTypeCode = SystemRelationshipTypeCode | string; +``` + +### RelationshipType + +```typescript +interface RelationshipType { + id: string; + code: string; + name: string; + forwardVerb: string; // "supports", "advises", "mentors" + reverseVerb: string; // "is supported by", "is advised by", "is mentored by" + allowsMultiple: boolean; + isActive: boolean; + isSystem: boolean; // System types cannot be deleted +} +``` + +### RoleNode (Org Chart) + +```typescript +interface RoleNode { + role: { + id: string; + code: string; + name: string; + roleType: RoleType; + }; + occupant: { + id: string | null; + name: string | null; + title: string | null; + avatarUrl: string | null; + } | null; + isVacant: boolean; + depth: number; + children: RoleNode[]; +} +``` + +### PositionNode (Org Chart - Future) + +```typescript +interface PositionNode { + position: { + id: string; + name: string; + roleName: string; + roleTypeCode: string; + organizationUnitName: string; + }; + occupant: { + id: string | null; + name: string | null; + title: string | null; + avatarUrl: string | null; + } | null; + status: PositionStatus; + depth: number; + children: PositionNode[]; +} +``` + +### Template Categories + +| Code | Name | Description | +|------|------|-------------| +| STARTUP | Startup | Basic structure for small companies | +| SMB | Small/Medium Business | Expanded structure with departments | +| ENTERPRISE | Enterprise | Comprehensive multi-level hierarchy | +| EOS | EOS® | Entrepreneurial Operating System structure | +| SCALING | Scaling Up | Based on Scaling Up methodology | + +--- + +## Seeded Role Types + +| Code | Name | Description | Is Stretch Role | +|------|------|-------------|-----------------| +| EXECUTIVE | Executive | C-level and senior leadership roles | No | +| MANAGEMENT | Management | Directors, VPs, and managers | No | +| PROFESSIONAL | Professional | Individual contributors with expertise | No | +| ASSOCIATE | Associate | Entry-level and support roles | No | + +--- + +## Seeded Organization Unit Types + +| Code | Name | Description | +|------|------|-------------| +| COMPANY | Company | Top-level organizational unit | +| DIVISION | Division | Major business unit or division | +| DEPARTMENT | Department | Functional department | +| TEAM | Team | Work team or unit | +| PROJECT | Project | Cross-functional project team | + +--- + +## Seeded Relationship Types + +| Code | Name | Forward Verb | Reverse Verb | Allows Multiple | +|------|------|--------------|--------------|----------------| +| SUPPORT | Support | supports | is supported by | Yes | +| ADVISE | Advise | advises | is advised by | Yes | +| COLLABORATE | Collaborate | collaborates with | collaborates with | Yes | +| MENTOR | Mentor | mentors | is mentored by | Yes | + +**Note:** "Reports To" is NOT a relationship type. Reporting hierarchy is defined directly on the Role entity via the `reportsToRoleId` field. This keeps the hierarchy simple and unidirectional. + +--- + +## Frontend Implementation Notes + +### Org Chart Visualization + +```typescript +// React component with react-organizational-chart +import { Tree, TreeNode } from 'react-organizational-chart'; + +const OrgChartNode = ({ role, occupant, isVacant, children }) => ( + +
{role.name}
+ {occupant ? ( +
{occupant.name}
+ ) : ( +
Vacant
+ )} +
+ }> + {children.map(child => ( + + ))} + +); +``` + +### Circular Reference Prevention (Frontend - Reports To Only) + +```typescript +// Before setting reportsToRoleId, check for cycles in the hierarchy +const wouldCreateCycle = ( + roleId: string, + newReportsToId: string, + roles: Role[] +): boolean => { + const visited = new Set(); + let current = newReportsToId; + + while (current) { + if (current === roleId) return true; // Found cycle! + if (visited.has(current)) break; // Already visited, no cycle to us + visited.add(current); + + // Find what this role reports to + const role = roles.find(r => r.id === current); + current = role?.reportsToRoleId || ''; + } + + return false; +}; + +// Usage in form validation +const handleReportsToChange = (newReportsToId: string) => { + if (wouldCreateCycle(editingRole.id, newReportsToId, allRoles)) { + showError('This would create a circular reporting structure'); + return; + } + // Proceed with update... +}; +``` + +**Note:** This validation only applies to `reportsToRoleId` on the Role entity. Role relationships (SUPPORT, ADVISE, etc.) are not hierarchical and don't need cycle detection. + +--- + +## WebSocket Events (Future) + +The following real-time events will be supported: + +| Event | Payload | Description | +|-------|---------|-------------| +| `role.created` | RoleResponse | New role created | +| `role.updated` | RoleResponse | Role details changed | +| `role.deactivated` | { roleId } | Role deactivated | +| `role.assignment_changed` | { roleId, personId, action } | Person assigned/unassigned | +| `org_chart.updated` | - | Hierarchy changed | + +--- + +## Complete API Index + +### People Service (Part 1) + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/auth/login` | Login with username/password | +| POST | `/auth/forgot-username` | Request username reminder | +| PUT | `/user/username` | Change username | +| GET | `/people` | List people | +| GET | `/people/assignable` | Get assignable people for dropdowns | +| GET | `/people/{id}` | Get person details | +| POST | `/people` | Create person | +| PUT | `/people/{id}` | Update person | +| DELETE | `/people/{id}` | Delete person | +| POST | `/people/{id}/activate` | Activate person | +| POST | `/people/{id}/deactivate` | Deactivate person | +| POST | `/people/{id}/link-user` | Link person to user | +| POST | `/people/{id}/tags` | Add tags | +| DELETE | `/people/{id}/tags/{tagId}` | Remove tag | +| GET | `/people/{id}/roles` | Get person's roles | +| GET | `/people/{id}/roles/history` | Get role history | +| POST | `/people/{id}/roles` | Assign role | +| PUT | `/people/{id}/roles/{roleId}/primary` | Set primary role | +| DELETE | `/people/{id}/roles/{roleId}` | Unassign role | +| GET | `/person-types` | List person types | +| GET | `/person-types/{id}` | Get person type | +| POST | `/person-types` | Create person type | +| PUT | `/person-types/{id}` | Update person type | +| DELETE | `/person-types/{id}` | Delete person type | +| POST | `/person-types/{id}/activate` | Activate person type | +| GET | `/person-tags` | List tags | +| POST | `/person-tags` | Create tag | +| PUT | `/person-tags/{id}` | Update tag | +| DELETE | `/person-tags/{id}` | Delete tag | + +### Organizational Structure (Part 2) + +#### Roles + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/roles` | List roles | +| GET | `/roles/dropdown` | Get roles for dropdown | +| GET | `/roles/{id}` | Get role details | +| POST | `/roles` | Create role | +| PUT | `/roles/{id}` | Update role | +| DELETE | `/roles/{id}` | Delete role | +| POST | `/roles/{id}/activate` | Activate role | +| POST | `/roles/{id}/deactivate` | Deactivate role | + +#### Role Types + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/role-types` | List role types | +| GET | `/role-types/{id}` | Get role type details | +| POST | `/role-types` | Create role type | +| PUT | `/role-types/{id}` | Update role type | +| DELETE | `/role-types/{id}` | Delete role type | + +#### Role Relationships + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/role-relationships` | List relationships | +| POST | `/role-relationships` | Create relationship | +| PUT | `/role-relationships/{id}` | Update relationship | +| DELETE | `/role-relationships/{id}` | Delete relationship | + +#### Role Relationship Types + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/role-relationship-types` | List relationship types | +| GET | `/role-relationship-types/{id}` | Get relationship type details | +| POST | `/role-relationship-types` | Create relationship type | +| PUT | `/role-relationship-types/{id}` | Update relationship type | +| DELETE | `/role-relationship-types/{id}` | Delete relationship type | + +#### Organization Units + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/organization-units` | List organization units | +| GET | `/organization-units/tree` | Get org unit hierarchy tree | +| GET | `/organization-units/dropdown` | Get simplified list for dropdowns | +| GET | `/organization-units/{id}` | Get organization unit details | +| POST | `/organization-units` | Create organization unit | +| PUT | `/organization-units/{id}` | Update organization unit | +| PUT | `/organization-units/{id}/status` | Update organization unit status | +| DELETE | `/organization-units/{id}` | Delete organization unit | + +#### Organization Unit Types + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/organization-unit-types` | List org unit types | +| GET | `/organization-unit-types/{id}` | Get org unit type details | +| POST | `/organization-unit-types` | Create org unit type | +| PUT | `/organization-unit-types/{id}` | Update org unit type | +| DELETE | `/organization-unit-types/{id}` | Delete org unit type | + +#### Positions + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/positions` | List positions | +| GET | `/positions/dropdown` | Get simplified list for dropdowns | +| GET | `/positions/tree` | Get hierarchical position tree | +| GET | `/positions/{id}` | Get position details | +| POST | `/positions` | Create position | +| PUT | `/positions/{id}` | Update position | +| DELETE | `/positions/{id}` | Delete position | +| POST | `/positions/{id}/assign` | Assign person to position | +| POST | `/positions/{id}/unassign` | Remove person from position | +| PUT | `/positions/{id}/status` | Update position status | + +#### Position Relationships + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/position-relationships` | List position relationships | +| POST | `/position-relationships` | Create position relationship | +| PUT | `/position-relationships/{id}` | Update position relationship | +| DELETE | `/position-relationships/{id}` | Delete position relationship | + +#### Org Chart + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/org-chart` | Get org chart tree | +| GET | `/org-chart/flat` | Get flat org chart | +| GET | `/org-chart/role/{id}/subtree` | Get role subtree | +| POST | `/roles/preview-template` | Preview template application | +| POST | `/roles/apply-template` | Apply template | + +> **Note:** Role Templates management (Admin Portal) is documented in [admin-api-specification.md](../admin-portal/admin-api-specification.md) + +--- + +**Document End** + +[← Part 1: People](./people-service.md) | [Back to Index](./index.md) diff --git a/docs/shared/Specifications/user-app/people-service.md b/docs/shared/Specifications/user-app/people-service.md new file mode 100644 index 00000000..8c8bd382 --- /dev/null +++ b/docs/shared/Specifications/user-app/people-service.md @@ -0,0 +1,1488 @@ +# People & Organizational Structure - Backend Integration Specifications (Part 1: People) + +**Version:** 2.0 +**Last Updated:** December 26, 2025 +**Service Base URL:** `{REACT_APP_ACCOUNT_API_URL}` (People & Auth endpoints) +**Default (Localhost):** `http://localhost:8001` (Account) + +[← Back to Index](./index.md) | [Part 2: Organizational Structure →](./org-structure-service.md) + +## Changelog + +| Version | Date | Changes | +|---------|------|----------| +| 2.0 | December 26, 2025 | **BREAKING:** Converted all JSON properties from snake_case to camelCase to match C#/.NET implementation (e.g., `person_id` → `personId`, `first_name` → `firstName`). Query parameters also converted to camelCase. This matches ASP.NET Core default JSON serialization. | +| 1.1 | December 23, 2025 | Migrated People endpoints from Traction service to Account service | +| 1.0 | December 21, 2025 | Initial version | + +--- + +## Overview + +The People module manages all person records within a tenant, including employees, consultants, vendors, and other stakeholders. This module also handles Person Types, Tags, and the User-Person linking that enables work item assignments. + +### Frontend Implementation + +- **Primary Client:** `accountClient` (axios instance) ⚠️ *Changed from tractionClient* +- **Related Files:** + - `src/services/people-service.ts` - Person CRUD operations + - `src/services/person-types-service.ts` - Person type management + - `src/services/person-tags-service.ts` - Tag management + +### Key Concepts + +- **Person**: Any individual related to the business (may or may not have system access) +- **User**: A Person with PurposePath login credentials +- **PersonType**: Category defining relationship to business (Employee, Vendor, etc.) +- **Tag**: Flexible label for grouping/filtering people +- **Assignable**: Flag indicating if a person can receive work assignments + +### Email Verification + +A Person's `isEmailVerified` flag is set to `true` when: +1. They accept a user invitation and create their account (primary flow) +2. They complete email verification during password reset + +There is no explicit `POST /people/{id}/verify-email` endpoint. Email verification is implicitly handled through the user invitation acceptance flow. + +--- + +## Authentication Changes + +### POST /auth/login + +User authentication with username/password. + +**⚠️ BREAKING CHANGE:** Login now uses `username` instead of `email`. + +**Request:** + +```json +{ + "username": "string", + "password": "string" +} +``` + +**Field Constraints:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| username | string | Yes | 3-50 characters | +| password | string | Yes | Min 8 characters | + +**Response:** + +```json +{ + "success": true, + "data": { + "accessToken": "string", + "refreshToken": "string", + "user": { + "userId": "string (GUID)", + "username": "string", + "personId": "string (GUID)", + "tenantId": "string (GUID)", + "avatarUrl": "string?", + "createdAt": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)", + "status": "active | inactive | locked", + "preferences": {} + }, + "person": { + "id": "string (GUID)", + "firstName": "string", + "lastName": "string", + "email": "string?", + "phone": "string?", + "title": "string?" + }, + "tenant": { + "id": "string (GUID)", + "name": "string" + } + } +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Invalid username or password", + "code": "INVALID_CREDENTIALS" +} +``` + +```json +{ + "success": false, + "error": "Account is locked", + "code": "ACCOUNT_LOCKED", + "details": { + "lockedUntil": "2025-12-22T10:30:00Z" + } +} +``` + +**Frontend Handling:** + +- Stores `accessToken` → `localStorage.accessToken` +- Stores `refreshToken` → `localStorage.refreshToken` +- Stores `tenant.id` → `localStorage.tenantId` +- Person data available for profile display + +--- + +### POST /auth/forgot-username + +Request username reminder via email. + +**Request:** + +```json +{ + "email": "string" +} +``` + +**Field Constraints:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| email | string | Yes | Valid email format | + +**Response (Always 200 to prevent email enumeration):** + +```json +{ + "success": true, + "message": "If the email is associated with any accounts, instructions have been sent." +} +``` + +**Backend Behavior:** + +1. Find all Persons with verified email matching input +2. For each Person linked to a User, include the username +3. Send email listing all associated usernames and tenant names +4. Always return success (even if email not found) + +--- + +### PUT /user/username + +Change current user's username. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Request:** + +```json +{ + "newUsername": "string", + "currentPassword": "string" +} +``` + +**Field Constraints:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| newUsername | string | Yes | 3-50 chars, alphanumeric + `.` `_` `-` `@`, must start with alphanumeric | +| currentPassword | string | Yes | Current password for verification | + +**Response:** + +```json +{ + "success": true, + "data": { + "username": "string", + "previousUsername": "string", + "nextChangeAllowedAt": "string (ISO 8601)" + } +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Username is already taken", + "code": "DUPLICATE_RESOURCE", + "details": { + "field": "newUsername" + } +} +``` + +```json +{ + "success": false, + "error": "Username can only be changed once every 30 days", + "code": "RATE_LIMIT_EXCEEDED", + "details": { + "nextChangeAllowedAt": "2025-01-15T10:30:00Z" + } +} +``` + +```json +{ + "success": false, + "error": "Invalid current password", + "code": "INVALID_CREDENTIALS" +} +``` + +**Business Rules:** + +- Username must be globally unique (case-insensitive) +- Maximum one change per 30 days +- Previous username reserved for 90 days (cannot be claimed by others) +- Email notification sent to linked Person's email + +--- + +## People Endpoints + +### GET /people + +List people with filtering and pagination. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| status | string | `active` | Filter: `active`, `inactive`, `all` | +| isAssignable | boolean | - | Filter by assignable flag | +| personTypeId | string (GUID) | - | Filter by person type | +| tags | string | - | Comma-separated tag GUIDs (any match) | +| search | string | - | Search by name, email, or title | +| page | number | 1 | Page number (1-based) | +| pageSize | number | 20 | Items per page (max 100) | +| sortBy | string | `name` | Sort field: `name`, `createdAt`, `type` | +| sortOrder | string | `asc` | Sort order: `asc`, `desc` | + +**Response:** + +```json +{ + "success": true, + "data": { + "items": [ + { + "id": "string (GUID)", + "firstName": "string", + "lastName": "string", + "email": "string?", + "isEmailVerified": "boolean", + "phone": "string?", + "title": "string?", + "personType": { + "id": "string (GUID)", + "code": "string", + "name": "string" + }, + "isActive": "boolean", + "isAssignable": "boolean", + "primaryRole": { + "id": "string (GUID)", + "name": "string" + }, + "tags": [ + { + "id": "string (GUID)", + "name": "string" + } + ], + "hasSystemAccess": "boolean", + "createdAt": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)?" + } + ], + "pagination": { + "page": "number", + "pageSize": "number", + "totalItems": "number", + "totalPages": "number" + } + } +} +``` + +--- + +### GET /people/assignable + +Get list of people available for work assignment dropdowns. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| tags | string | - | Comma-separated tag GUIDs (any match) | +| personTypeId | string (GUID) | - | Filter by person type | +| search | string | - | Search by name | + +**Response:** + +```json +{ + "success": true, + "data": [ + { + "id": "string (GUID)", + "name": "string", + "title": "string?", + "primaryRole": "string?", + "isCurrentUser": "boolean" + } + ] +} +``` + +**Notes:** + +- Returns only people where `isActive = true` AND `isAssignable = true` +- `isCurrentUser` is `true` if this Person is linked to the authenticated User +- Sorted alphabetically by name, with current user first + +--- + +### GET /people/{id} + +Get detailed person information. + +**Path Parameters:** + +- `id` - Person ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "firstName": "string", + "lastName": "string", + "email": "string?", + "isEmailVerified": "boolean", + "phone": "string?", + "title": "string?", + "personType": { + "id": "string (GUID)", + "code": "string", + "name": "string" + }, + "isActive": "boolean", + "isAssignable": "boolean", + "notes": "string?", + "tags": [ + { + "id": "string (GUID)", + "name": "string" + } + ], + "roles": [ + { + "id": "string (GUID)", + "roleId": "string (GUID)", + "roleCode": "string", + "roleName": "string", + "isPrimary": "boolean", + "effectiveDate": "string (ISO 8601)", + "terminationDate": "string (ISO 8601)?" + } + ], + "linkedUserId": "string (GUID)?", + "hasSystemAccess": "boolean", + "createdAt": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)?", + "createdBy": "string (GUID)", + "updatedBy": "string (GUID)?" + } +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Person not found", + "code": "RESOURCE_NOT_FOUND", + "details": { + "resourceType": "Person", + "resourceId": "guid" + } +} +``` + +--- + +### POST /people + +Create a new person. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Request:** + +```json +{ + "firstName": "string", + "lastName": "string", + "email": "string?", + "phone": "string?", + "title": "string?", + "personTypeId": "string (GUID)", + "isAssignable": "boolean?", + "notes": "string?", + "tags": ["string (GUID)"] +} +``` + +**Field Constraints:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| firstName | string | Yes | 1-100 characters | +| lastName | string | Yes | 1-100 characters | +| email | string | No* | Valid email format, unique within tenant | +| phone | string | No | Max 20 characters | +| title | string | No | Max 100 characters | +| personTypeId | GUID | Yes | Must exist and be active | +| isAssignable | boolean | No | Defaults from PersonType.isAssignableByDefault | +| notes | string | No | Max 2000 characters | +| tags | GUID[] | No | Must be valid tag IDs | + +*Email is required if person will be linked to a User. + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "firstName": "string", + "lastName": "string", + "email": "string?", + "isEmailVerified": false, + "phone": "string?", + "title": "string?", + "personType": { + "id": "string (GUID)", + "code": "string", + "name": "string" + }, + "isActive": true, + "isAssignable": "boolean", + "notes": "string?", + "tags": [], + "roles": [], + "linkedUserId": null, + "hasSystemAccess": false, + "createdAt": "string (ISO 8601)", + "createdBy": "string (GUID)" + } +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Email is already in use within this tenant", + "code": "DUPLICATE_RESOURCE", + "details": { + "field": "email" + } +} +``` + +```json +{ + "success": false, + "error": "Person type not found or inactive", + "code": "VALIDATION_ERROR", + "details": { + "field": "personTypeId" + } +} +``` + +--- + +### PUT /people/{id} + +Update an existing person. + +**Path Parameters:** + +- `id` - Person ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Request:** + +```json +{ + "firstName": "string?", + "lastName": "string?", + "email": "string?", + "phone": "string?", + "title": "string?", + "personTypeId": "string (GUID)?", + "isAssignable": "boolean?", + "notes": "string?" +} +``` + +**Notes:** + +- All fields are optional (partial update) +- Cannot update `isActive` via this endpoint (use activate/deactivate) +- Tags managed via separate endpoints + +**Response:** + +```json +{ + "success": true, + "data": "PersonResponse (full object)" +} +``` + +--- + +### DELETE /people/{id} + +Soft delete (deactivate) a person. Optionally reassign their work items to another person. + +**Path Parameters:** + +- `id` - Person ID (GUID) + +**Query Parameters:** + +- `reassignTo` - Optional: Person ID (GUID) to reassign work items to + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "message": "Person deactivated successfully", + "data": { + "id": "string (GUID)", + "firstName": "string", + "lastName": "string", + "displayName": "string", + "email": "string?", + "phone": "string?", + "title": "string?", + "personType": { + "id": "string (GUID)", + "name": "string" + }, + "isAssignable": false, + "isPrimary": false, + "notes": "string?", + "status": "inactive", + "linkedUserId": "string (GUID)?", + "isEmailVerified": false, + "tags": ["string (GUID)"], + "roleAssignments": [], + "createdBy": "string (GUID)", + "createdAt": "string (ISO 8601)", + "updatedBy": "string (GUID)", + "updatedAt": "string (ISO 8601)" + } +} +``` + +**Error Responses:** + +- **404 Not Found** - Person not found +- **400 Bad Request** - Invalid person ID format, invalid reassignTo ID, or deactivation failed +- **500 Internal Server Error** - Server error + +--- + +### POST /people/{id}/activate + +Reactivate a deactivated person. + +**Path Parameters:** + +- `id` - Person ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "message": "Person activated successfully", + "data": { + "id": "string (GUID)", + "firstName": "string", + "lastName": "string", + "displayName": "string", + "email": "string?", + "phone": "string?", + "title": "string?", + "personType": { + "id": "string (GUID)", + "name": "string" + }, + "isAssignable": true, + "isPrimary": false, + "notes": "string?", + "status": "active", + "linkedUserId": "string (GUID)?", + "isEmailVerified": false, + "tags": ["string (GUID)"], + "roleAssignments": [], + "createdBy": "string (GUID)", + "createdAt": "string (ISO 8601)", + "updatedBy": "string (GUID)", + "updatedAt": "string (ISO 8601)" + } +} +``` + +**Error Responses:** + +- **404 Not Found** - Person not found +- **400 Bad Request** - Invalid person ID format or activation failed +- **500 Internal Server Error** - Server error + +--- + +### POST /people/{id}/link-user + +Link a person to an existing user account. + +**Path Parameters:** + +- `id` - Person ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Request:** + +```json +{ + "userId": "string (GUID)" +} +``` + +**Response:** + +```json +{ + "success": true, + "data": { + "personId": "string (GUID)", + "userId": "string (GUID)", + "linkedAt": "string (ISO 8601)" + } +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Person is already linked to a user", + "code": "BUSINESS_RULE_VIOLATION", + "details": { + "existingUserId": "guid" + } +} +``` + +```json +{ + "success": false, + "error": "User is already linked to another person", + "code": "BUSINESS_RULE_VIOLATION", + "details": { + "existingPersonId": "guid" + } +} +``` + +```json +{ + "success": false, + "error": "Person must have a verified email to be linked to a user", + "code": "VALIDATION_ERROR" +} +``` + +--- + +### POST /people/{id}/tags + +Add tags to a person. + +**Path Parameters:** + +- `id` - Person ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Request:** + +```json +{ + "tagIds": ["string (GUID)"] +} +``` + +**Response:** + +```json +{ + "success": true, + "data": { + "tags": [ + { + "id": "string (GUID)", + "name": "string", + "assignedAt": "string (ISO 8601)" + } + ] + } +} +``` + +--- + +### DELETE /people/{id}/tags/{tagId} + +Remove a tag from a person. + +**Path Parameters:** + +- `id` - Person ID (GUID) +- `tagId` - Tag ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true +} +``` + +--- + +## Person Roles Endpoints + +### GET /people/{id}/roles + +Get person's current role assignments. + +**Path Parameters:** + +- `id` - Person ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "data": [ + { + "id": "string (GUID)", + "role": { + "id": "string (GUID)", + "code": "string", + "name": "string" + }, + "isPrimary": "boolean", + "effectiveDate": "string (ISO 8601)", + "terminationDate": null + } + ] +} +``` + +--- + +### GET /people/{id}/roles/history + +Get person's complete role assignment history. + +**Path Parameters:** + +- `id` - Person ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "data": { + "current": [ + { + "id": "string (GUID)", + "role": { + "id": "string (GUID)", + "code": "string", + "name": "string" + }, + "isPrimary": "boolean", + "effectiveDate": "string (ISO 8601)" + } + ], + "historical": [ + { + "id": "string (GUID)", + "role": { + "id": "string (GUID)", + "code": "string", + "name": "string" + }, + "isPrimary": "boolean", + "effectiveDate": "string (ISO 8601)", + "terminationDate": "string (ISO 8601)" + } + ] + } +} +``` + +--- + +### POST /people/{id}/roles + +Assign a role to a person. + +**Path Parameters:** + +- `id` - Person ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Request:** + +```json +{ + "roleId": "string (GUID)", + "isPrimary": "boolean?", + "effectiveDate": "string (ISO 8601)?" +} +``` + +**Field Constraints:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| roleId | GUID | Yes | Must be active role | +| isPrimary | boolean | No | Default: `true` if first role, else `false` | +| effectiveDate | ISO 8601 | No | Default: current date | + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "role": { + "id": "string (GUID)", + "code": "string", + "name": "string" + }, + "isPrimary": "boolean", + "effectiveDate": "string (ISO 8601)", + "previousOccupantTerminated": "boolean" + } +} +``` + +**Notes:** + +- If role already has an occupant, their assignment is auto-terminated +- `previousOccupantTerminated` indicates if this happened + +**Error Responses:** + +```json +{ + "success": false, + "error": "Person is not active", + "code": "BUSINESS_RULE_VIOLATION" +} +``` + +```json +{ + "success": false, + "error": "Role not found or inactive", + "code": "VALIDATION_ERROR", + "details": { + "field": "roleId" + } +} +``` + +--- + +### PUT /people/{id}/roles/{roleId}/primary + +Set a role as the person's primary role. + +**Path Parameters:** + +- `id` - Person ID (GUID) +- `roleId` - Role ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "data": { + "previousPrimaryRoleId": "string (GUID)?", + "newPrimaryRoleId": "string (GUID)" + } +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Person is not assigned to this role", + "code": "VALIDATION_ERROR" +} +``` + +--- + +### DELETE /people/{id}/roles/{roleId} + +Unassign a role from a person. + +**Path Parameters:** + +- `id` - Person ID (GUID) +- `roleId` - Role ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| terminationDate | ISO 8601 | now | When assignment ends | + +**Response:** + +```json +{ + "success": true, + "data": { + "terminated": true, + "terminationDate": "string (ISO 8601)", + "wasPrimary": "boolean", + "newPrimaryRoleId": "string (GUID)?" + } +} +``` + +**Notes:** + +- If this was the primary role and person has other roles, frontend should prompt to select new primary + +--- + +## Person Types Endpoints + +### GET /person-types + +List all person types for the tenant. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Query Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| includeInactive | boolean | false | Include inactive types | + +**Response:** + +```json +{ + "success": true, + "data": [ + { + "id": "string (GUID)", + "code": "string", + "name": "string", + "description": "string?", + "isAssignableByDefault": "boolean", + "displayOrder": "number", + "isActive": "boolean", + "createdAt": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)?" + } + ] +} +``` + +--- + +### GET /person-types/{id} + +Get person type details. + +**Path Parameters:** + +- `id` - PersonType ID (GUID) + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "code": "string", + "name": "string", + "description": "string?", + "isAssignableByDefault": "boolean", + "displayOrder": "number", + "isActive": "boolean", + "personCount": "number", + "createdAt": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)?" + } +} +``` + +--- + +### POST /person-types + +Create a new person type. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Request:** + +```json +{ + "code": "string", + "name": "string", + "description": "string?", + "isAssignableByDefault": "boolean", + "displayOrder": "number?" +} +``` + +**Field Constraints:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| code | string | Yes | 2-20 chars, uppercase alphanumeric + underscore, unique within tenant | +| name | string | Yes | 1-100 characters | +| description | string | No | Max 500 characters | +| isAssignableByDefault | boolean | Yes | - | +| displayOrder | number | No | Default: next available | + +**Response:** + +```json +{ + "success": true, + "data": "PersonTypeResponse" +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Person type code already exists", + "code": "DUPLICATE_RESOURCE", + "details": { + "field": "code" + } +} +``` + +--- + +### PUT /person-types/{id} + +Update a person type. + +**Path Parameters:** + +- `id` - PersonType ID (GUID) + +**Request:** + +```json +{ + "name": "string?", + "description": "string?", + "isAssignableByDefault": "boolean?", + "displayOrder": "number?" +} +``` + +**Notes:** + +- `code` cannot be changed after creation +- Changes to `isAssignableByDefault` do not affect existing persons + +**Response:** + +```json +{ + "success": true, + "data": "PersonTypeResponse" +} +``` + +--- + +### DELETE /person-types/{id} + +Deactivate a person type. + +**Path Parameters:** + +- `id` - PersonType ID (GUID) + +**Response:** + +```json +{ + "success": true +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Cannot delete person type with existing persons", + "code": "BUSINESS_RULE_VIOLATION", + "details": { + "personCount": 15 + } +} +``` + +--- + +## Person Tags Endpoints + +### GET /api/person-tags + +List all tags for the tenant. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "data": [ + { + "id": "string (GUID)", + "name": "string", + "personCount": "number", + "createdAt": "string (ISO 8601)" + +--- + +### POST /person-types/{id}/activate + +Reactivate a deactivated person type. + +**Path Parameters:** + +- `id` - PersonType ID (GUID) + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "code": "string", + "name": "string", + "description": "string?", + "isAssignableByDefault": "boolean", + "displayOrder": "number", + "isActive": "boolean", + "createdAt": "string (ISO 8601)", + "updatedAt": "string (ISO 8601)?" + } +} +``` + +**Error Responses:** + +- **404 Not Found** - Person type not found +- **400 Bad Request** - Invalid person type ID format or activation failed +- **500 Internal Server Error** - Server error + } + ] +} +``` + +--- + +### POST /api/person-tags + +Create a new tag. + +**Headers Required:** + +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Request:** + +```json +{ + "name": "string" +} +``` + +**Field Constraints:** + +| Field | Type | Required | Constraints | +|-------|------|----------|-------------| +| name | string | Yes | 1-50 characters, unique within tenant | + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "name": "string", + "createdAt": "string (ISO 8601)" + } +} +``` + +**Error Responses:** + +```json +{ + "success": false, + "error": "Tag name already exists", + "code": "DUPLICATE_RESOURCE", + "details": { + "field": "name" + } +} +``` + +--- + +### PUT /api/person-tags/{id} + +Update a tag name. + +**Path Parameters:** + +- `id` - Tag ID (GUID) + +**Request:** + +```json +{ + "name": "string" +} +``` + +**Response:** + +```json +{ + "success": true, + "data": { + "id": "string (GUID)", + "name": "string" + } +} +``` + +--- + +### DELETE /api/person-tags/{id} + +Delete a tag (cascade removes all person-tag assignments). + +**Path Parameters:** + +- `id` - Tag ID (GUID) + +**Response:** + +```json +{ + "success": true, + "data": { + "assignmentsRemoved": "number" + } +} +``` + +--- + +## Error Codes Reference + +| Code | HTTP Status | Description | +|------|-------------|-------------| +| `VALIDATION_ERROR` | 400 | Input validation failed | +| `INVALID_CREDENTIALS` | 401 | Wrong username/password | +| `UNAUTHORIZED` | 401 | Missing/invalid token | +| `FORBIDDEN` | 403 | Insufficient permissions | +| `RESOURCE_NOT_FOUND` | 404 | Resource does not exist | +| `DUPLICATE_RESOURCE` | 409 | Resource already exists (unique constraint) | +| `BUSINESS_RULE_VIOLATION` | 400 | Domain rule prevents operation | +| `RATE_LIMIT_EXCEEDED` | 429 | Too many requests | +| `ACCOUNT_LOCKED` | 403 | User account is locked | + +--- + +## Data Types Reference + +### PersonStatus + +```typescript +type PersonStatus = 'active' | 'inactive'; +``` + +### Username Validation + +```typescript +const USERNAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._@-]{2,49}$/; +const RESERVED_USERNAMES = ['admin', 'system', 'support', 'purposepath', 'help', 'info']; +``` + +### Default Person Types (Seeded) + +| Code | Name | Assignable | +|------|------|------------| +| EMPLOYEE | Employee | Yes | +| CONSULTANT | Consultant | Yes | +| VENDOR | Vendor | No | +| PARTNER | Partner | No | +| ADVISOR | Advisor | No | +| BOARD | Board Member | No | + +--- + +**Document End** + +[← Back to Index](./index.md) | [Part 2: Organizational Structure →](./org-structure-service.md) diff --git a/docs/shared/Specifications/user-app/traction-service/README.md b/docs/shared/Specifications/user-app/traction-service/README.md new file mode 100644 index 00000000..612655bc --- /dev/null +++ b/docs/shared/Specifications/user-app/traction-service/README.md @@ -0,0 +1,227 @@ +# Traction Service API Specifications (v7) + +**Version:** 7.0 +**Last Updated:** December 23, 2025 +**Service Base URL:** `{REACT_APP_TRACTION_API_URL}` +**Default (Localhost):** `http://localhost:8002` + +[← Back to Main Index](../index.md) + +--- + +## Overview + +This directory contains detailed API specifications for the Traction Service, organized by controller/feature area for easier maintenance and reference. + +**Version 7 Changes:** +- Removed all deprecated GoalMeasureLink, MeasureMilestone, MeasureActual, and MeasureReading endpoints +- Documented only actively implemented endpoints +- Split large specification into maintainable controller-based documents +- Updated to reflect MeasureLink and MeasureData design (Epic #362) + +--- + +## Authentication & Headers + +**All endpoints require:** +- `Authorization: Bearer {accessToken}` - JWT token from authentication +- `X-Tenant-Id: {tenantId}` - Multi-tenancy identifier (UUID) + +**Optional headers:** +- `X-Frontend-Base-Url` - Base URL for email links (used by some endpoints) + +--- + +## API Structure + +### Controller-Based Specifications + +| Document | Controller | Endpoints | Description | +|----------|------------|-----------|-------------| +| [Goals API](./goals-api.md) | GoalsController | 11 endpoints | Goal lifecycle management | +| [Strategies API](./strategies-api.md) | StrategiesController | 6 endpoints | Strategy management and alignment | +| [Alignment API](./alignment-api.md) | AlignmentController | 1 endpoint | Cached goal alignment lookup | +| [Measures API](./measures-api.md) | MeasuresController | 7 endpoints | Measure instance management | +| [Measure Links API](./measure-links-api.md) | MeasureLinksController | 6 endpoints | Link Measures to goals, people, strategies | +| [Measure Data API](./measure-data-api.md) | MeasureDataController | 8 endpoints | Targets, actuals, projections | +| [Actions API](./actions-api.md) | ActionsController | 10 endpoints | Action items management | +| [Issues API](./issues-api.md) | IssuesController | 5 endpoints | Issue tracking | +| [Coaching Insights API](./insights-api.md) | InsightsController | 5 endpoints | AI-generated coaching insights (CRUD) | +| [Dashboard, Reports & Activities](./dashboard-reports-activities-api.md) | Multiple | 5 endpoints | Command center, reports, activity feeds | + +> **Note:** People API has been deferred and moved to Account Service for better organizational alignment. + +### Reference Data APIs + +> **Note:** Reference data endpoints (issue types, statuses, tags) are integrated within their respective API documents above. + +--- + +## Common Patterns + +### Response Structure + +All endpoints return a consistent wrapper: + +```json +{ + "success": true, + "data": { /* actual response data */ }, + "error": null, + "timestamp": "2025-12-23T10:30:00Z" +} +``` + +### Pagination + +List endpoints support pagination: + +**Query Parameters:** +- `page` (number, default: 1) - Page number (1-indexed) +- `pageSize` (number, default: 20) - Items per page +- `sortBy` (string) - Field to sort by +- `sortOrder` (string) - "asc" or "desc" + +**Paginated Response:** +```json +{ + "success": true, + "data": { + "items": [ /* array of items */ ], + "pagination": { + "page": 1, + "pageSize": 20, + "total": 150, + "totalPages": 8 + } + } +} +``` + +### Error Responses + +```json +{ + "success": false, + "data": null, + "error": "Descriptive error message", + "timestamp": "2025-12-23T10:30:00Z" +} +``` + +**HTTP Status Codes:** +- `200` - Success +- `201` - Created +- `400` - Bad Request (validation errors) +- `401` - Unauthorized (missing/invalid token) +- `403` - Forbidden (insufficient permissions) +- `404` - Not Found +- `422` - Unprocessable Entity (business rule violation) +- `500` - Internal Server Error + +### Date/Time Format + +All dates use ISO 8601 format: `YYYY-MM-DDTHH:mm:ss.fffZ` + +Example: `"2025-12-23T15:30:00.000Z"` + +### Field Naming + +- **Frontend:** camelCase (e.g., `goalName`, `createdAt`) +- **Backend:** May use PascalCase internally but APIs use camelCase +- **Enums:** lowercase with underscores (e.g., `in_progress`, `on_track`) + +--- + +## Quick Reference + +### Most Common Endpoints + +| Operation | Method | Endpoint | +|-----------|--------|----------| +| List goals | GET | `/goals` | +| Create goal | POST | `/goals` | +| Get goal details | GET | `/goals/{id}` | +| Update goal | PUT | `/goals/{id}` | +| List Measures | GET | `/measures` | +| Create Measure | POST | `/measures` | +| Link Measure to goal | POST | `/measure-links` | +| Record Measure data | POST | `/measure-data` | +| List actions | GET | `/operations/actions` | +| Create action | POST | `/operations/actions` | +| List issues | GET | `/issues` | +| Create issue | POST | `/issues` | + +--- + +## Migration from v6 + +**Removed Endpoints (Deprecated):** +- All `/goals/{goalId}/measures:link` endpoints → Use `/measure-links` instead +- All `/goals/{goalId}/measures:unlink` endpoints → Use `/measure-links` DELETE +- All `/goals/{goalId}/available-measures` endpoints → New design pending +- All `/measure-planning/*` endpoints → Redesigned as `/measure-data` +- All shared Measure endpoints → Consolidated into `/measure-links` + +**New Endpoints (v7):** +- `/measure-links` - Unified Measure linking +- `/measure-data` - All Measure data operations (targets, actuals, projections) +- Simplified Measure management + +**Changed Endpoints:** +- Measure operations moved from goals context to dedicated Measure resources +- More RESTful resource design + +--- + +## Development Guidelines + +### Adding New Endpoints + +1. Update appropriate controller spec file +2. Document request/response structures completely +3. Include validation rules and constraints +4. Add examples +5. Update this index with endpoint count + +### Deprecating Endpoints + +1. Mark as deprecated in spec with migration path +2. Add comment to controller code +3. Update migration section in this README +4. Plan removal timeline + +### Maintaining Specifications + +- Specs should match actual implementation +- Update specs when code changes +- Review specs during code review +- Keep examples realistic + +--- + +## Version History + +### v7.0 (December 23, 2025) +- Removed deprecated Measure endpoints (GoalMeasureLink, MeasureMilestone, MeasureActual, MeasureReading) +- Split monolithic spec into controller-based documents +- Updated to reflect MeasureLink and MeasureData design +- 121 files changed, 15,902 lines of deprecated code removed + +### v6.0 (December 21, 2025) +- Added Measure linking endpoints +- Added Measure planning endpoints +- Documented nested object structures + +### v5.0 (Previous) +- Initial comprehensive specification + +--- + +## Contact & Support + +For questions about these specifications: +- Create a GitHub issue +- Tag with `documentation` label +- Reference the specific endpoint/controller + diff --git a/docs/shared/Specifications/user-app/traction-service/actions-api.md b/docs/shared/Specifications/user-app/traction-service/actions-api.md new file mode 100644 index 00000000..87321dfb --- /dev/null +++ b/docs/shared/Specifications/user-app/traction-service/actions-api.md @@ -0,0 +1,815 @@ +# Actions API Specification + +**Version:** 7.2 +**Last Updated:** February 9, 2026 +**Base Path:** `/operations/actions` +**Controller:** `ActionsController.cs` + +## Overview + +The Actions API manages action items (tasks/to-dos) within the PurposePath traction system. Actions can be linked to goals, strategies, and issues to track execution and progress. + +### Key Features +- Complete CRUD operations for action items +- Status management (not_started, in_progress, completed, blocked, cancelled) +- Priority levels (low, medium, high, critical) +- Link actions to goals, strategies, and issues +- Advanced filtering and pagination +- Progress tracking with estimated/actual hours +- Assignment to team members + +--- + +## Authentication + +All endpoints require: +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +--- + +## Endpoints + +### 1. List Actions + +Retrieve actions with advanced filtering and pagination. + +**Endpoint:** `GET /operations/actions` + +#### Query Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `status` | enum | No | Filter by status: `not_started`, `in_progress`, `completed`, `blocked`, `cancelled` | +| `priority` | enum | No | Filter by priority: `low`, `medium`, `high`, `critical` | +| `assignedPersonId` | string (GUID) | No | Filter by assignee | +| `goalIds` | string (CSV) | No | Comma-separated goal IDs to filter by | +| `strategyIds` | string (CSV) | No | Comma-separated strategy IDs | +| `issueIds` | string (CSV) | No | Comma-separated issue IDs | +| `startDate` | datetime (ISO 8601) | No | Filter actions starting after this date | +| `endDate` | datetime (ISO 8601) | No | Filter actions due before this date | +| `search` | string | No | Case-insensitive search in title, description, and tags | +| `page` | int | No | Page number (default: 1) | +| `limit` | int | No | Items per page (default: 50, max: 100) | +| `sort` | string | No | Sort field: `dueDate`, `startDate`, `priority`, `status`, `createdAt`, `title` | +| `order` | string | No | Sort order: `asc` or `desc` (default: `asc`) | + +#### Request Example + +```http +GET /operations/actions?status=in_progress&priority=high&assignedPersonId=person-123&search=pricing&page=1&limit=20&sort=startDate&order=asc +Authorization: Bearer {token} +X-Tenant-Id: {tenantId} +``` + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "success": true, + "data": [ + { + "id": "action-123e4567-e89b-12d3-a456-426614174000", + "tenantId": "tenant-123", + "title": "Launch new pricing page", + "description": "Design and deploy updated pricing page with new tier structure", + "status": "in_progress", + "priority": "high", + "startDate": "2025-12-20T00:00:00Z", + "dueDate": "2025-12-31T00:00:00Z", + "dateEntered": "2025-12-15T10:00:00Z", + "assignedPersonId": "person-123", + "assignedPersonName": "John Doe", + "progress": 65, + "estimatedHours": 40, + "actualHours": 28, + "tags": ["marketing", "website", "Q4"], + "connections": { + "goalIds": ["goal-001"], + "strategyIds": ["strategy-002"], + "issueIds": [] + }, + "createdAt": "2025-12-15T10:00:00Z", + "updatedAt": "2025-12-23T14:30:00Z", + "createdBy": "user-001", + "modifiedBy": "user-002", + "isDeleted": false + } + ], + "pagination": { + "page": 1, + "limit": 20, + "total": 45, + "totalPages": 3, + "hasNextPage": true, + "hasPreviousPage": false + } + }, + "error": null +} +``` + +#### Field Descriptions + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string (GUID) | Unique action identifier | +| `tenantId` | string | Organization identifier | +| `title` | string | Action title/summary (max 200 chars) | +| `description` | string | Detailed description (max 2000 chars) | +| `status` | enum | Current status (see Status enum below) | +| `priority` | enum | Priority level (see Priority enum below) | +| `startDate` | datetime | When action should start | +| `dueDate` | datetime | When action is due | +| `dateEntered` | datetime | When action was originally entered (may differ from createdAt) | +| `assignedPersonId` | string (GUID) | Person responsible for this action | +| `assignedPersonName` | string | Name of assigned person (read-only) | +| `progress` | int | Completion percentage (0-100) | +| `estimatedHours` | decimal | Estimated effort in hours | +| `actualHours` | decimal | Actual time spent in hours | +| `tags` | string[] | Tags for categorization/filtering | +| `connections` | object | Related entities (goals, strategies, issues) | +| `createdAt` | datetime | When action was created | +| `updatedAt` | datetime | Last update timestamp | +| `createdBy` | string (GUID) | User who created the action | +| `modifiedBy` | string (GUID) | User who last modified the action | +| `isDeleted` | boolean | Soft delete flag | + +#### Status Enum + +```typescript +enum ActionStatus { + NotStarted = "not_started", // Planned but not begun + InProgress = "in_progress", // Currently being worked on + Completed = "completed", // Finished successfully + Blocked = "blocked", // Blocked by dependency/issue + Cancelled = "cancelled" // Cancelled/abandoned +} +``` + +#### Priority Enum + +```typescript +enum Priority { + Low = "low", // Can wait + Medium = "medium", // Standard priority + High = "high", // Important, time-sensitive + Critical = "critical" // Urgent, must be done ASAP +} +``` + +--- + +### 2. Create Action + +Create a new action item. + +**Endpoint:** `POST /operations/actions` + +#### Request Body + +```json +{ + "title": "Launch new pricing page", + "description": "Design and deploy updated pricing page with new tier structure", + "startDate": "2025-12-20T00:00:00Z", + "dueDate": "2025-12-31T00:00:00Z", + "priority": "high", + "status": "in_progress", + "progress": 25, + "assignedPersonId": "person-123", + "estimatedHours": 40, + "tags": ["marketing", "website", "Q4"], + "dateEntered": "2025-12-15T10:00:00Z", + "connections": { + "goalIds": ["goal-001"], + "strategyIds": ["strategy-002"], + "issueIds": [] + } +} +``` + +#### Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `title` | string | **Yes** | Action title (max 200 chars) | +| `description` | string | No | Detailed description (max 2000 chars) | +| `startDate` | datetime (ISO 8601) | **Yes** | Start date | +| `dueDate` | datetime (ISO 8601) | **Yes** | Due date | +| `priority` | enum | **Yes** | `low`, `medium`, `high`, `critical` | +| `status` | enum | No | Initial status: `not_started`, `in_progress`, `completed`, `blocked`, `cancelled` (defaults to `not_started`) | +| `progress` | int | No | Initial progress percentage (0-100, defaults to 0) | +| `assignedPersonId` | string (GUID) | **Yes** | Person responsible | +| `estimatedHours` | decimal | No | Estimated effort in hours | +| `tags` | string[] | No | Tags for categorization | +| `dateEntered` | datetime | No | Original entry date (defaults to now) | +| `connections` | object | No | Link to goals, strategies, issues | +| `connections.goalIds` | string[] | No | Goal IDs to link | +| `connections.strategyIds` | string[] | No | Strategy IDs to link | +| `connections.issueIds` | string[] | No | Issue IDs to link | + +#### Response + +**Status:** `200 OK` (Note: Should be 201 Created per REST standards) + +```json +{ + "success": true, + "data": { + "id": "action-new-123", + "tenantId": "tenant-123", + "title": "Launch new pricing page", + "description": "Design and deploy updated pricing page with new tier structure", + "status": "in_progress", + "priority": "high", + "startDate": "2025-12-20T00:00:00Z", + "dueDate": "2025-12-31T00:00:00Z", + "assignedPersonId": "person-123", + "assignedPersonName": "John Doe", + "progress": 25, + "estimatedHours": 40, + "actualHours": 0, + "tags": ["marketing", "website", "Q4"], + "connections": { + "goalIds": ["goal-001"], + "strategyIds": ["strategy-002"], + "issueIds": [] + }, + "createdAt": "2025-12-23T16:45:00Z", + "updatedAt": "2025-12-23T16:45:00Z" + }, + "error": null +} +``` + +#### Business Rules + +- **Default status:** New actions default to `not_started` status if not provided +- **Default progress:** New actions default to 0% progress if not provided +- **Completed actions:** If status is `completed`, progress is automatically set to 100% regardless of provided value +- **Progress validation:** Progress must be between 0 and 100 (inclusive) +- **Date validation:** `dueDate` must be after `startDate` +- **Person validation:** `assignedPersonId` must exist in tenant +- **Connections:** Goals, strategies, issues are linked after action creation +- **Invalid connection IDs:** Invalid GUID formats are filtered out (action still created) + +--- + +### 3. Get Action + +Retrieve details of a specific action. + +**Endpoint:** `GET /operations/actions/{actionId}` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `actionId` | string (GUID) | **Yes** | Action identifier | + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "id": "action-123", + "title": "Launch new pricing page", + "status": "in_progress", + "priority": "high", + "progress": 65, + "estimatedHours": 40, + "actualHours": 28, + "connections": { + "goalIds": ["goal-001"], + "strategyIds": ["strategy-002"], + "issueIds": [] + } + }, + "error": null +} +``` + +#### Error Responses + +**Status:** `404 Not Found` +```json +{ + "success": false, + "data": null, + "error": "Action not found" +} +``` + +--- + +### 4. Update Action Status + +Update only the status of an action. + +**Endpoint:** `PUT /operations/actions/{actionId}/status` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `actionId` | string (GUID) | **Yes** | Action identifier | + +#### Request Body + +```json +{ + "status": "completed", + "reason": "All tasks completed successfully", + "actualHours": 35 +} +``` + +#### Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `status` | enum | **Yes** | New status: `not_started`, `in_progress`, `completed`, `blocked`, `cancelled` | +| `reason` | string | No | Reason for status change (recommended) | +| `actualHours` | decimal | No | Actual hours spent (usually provided when marking completed) | + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "id": "action-123", + "status": "completed", + "actualHours": 35, + "progress": 100, + "updatedAt": "2025-12-23T17:00:00Z" + }, + "error": null +} +``` + +#### Business Rules + +- **Auto-progress:** Completing an action automatically sets `progress` to 100 +- **Blocked actions:** Can include `reason` to explain blocker +- **Status transitions:** Any status can transition to any other status (no restrictions) + +--- + +### 5. Update Action + +Update action details (title, description, dates, priority, etc.). + +**Endpoint:** `PUT /operations/actions/{actionId}` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `actionId` | string (GUID) | **Yes** | Action identifier | + +#### Request Body + +```json +{ + "title": "Launch new pricing page (revised)", + "description": "Updated scope: Include testimonials section", + "startDate": "2025-12-20T00:00:00Z", + "dueDate": "2026-01-10T00:00:00Z", + "priority": "high", + "status": "in_progress", + "progress": 75, + "assignedPersonId": "person-456", + "estimatedHours": 50, + "actualHours": 32, + "tags": ["marketing", "website", "Q4", "testimonials"] +} +``` + +#### Request Fields + +All fields are optional. Only provided fields will be updated. + +| Field | Type | Description | +|-------|------|-------------| +| `title` | string | Updated title | +| `description` | string | Updated description | +| `startDate` | datetime | Updated start date | +| `dueDate` | datetime | Updated due date | +| `priority` | enum | Updated priority | +| `status` | enum | Updated status | +| `progress` | int | Updated progress (0-100) | +| `assignedPersonId` | string (GUID) | Reassign to different person | +| `estimatedHours` | decimal | Updated estimate | +| `actualHours` | decimal | Updated actual hours | +| `tags` | string[] | Updated tags | + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "id": "action-123", + "title": "Launch new pricing page (revised)", + "description": "Updated scope: Include testimonials section", + "dueDate": "2026-01-10T00:00:00Z", + "priority": "high", + "status": "in_progress", + "progress": 75, + "assignedPersonId": "person-456", + "updatedAt": "2025-12-23T17:15:00Z" + }, + "error": null +} +``` + +#### Business Rules + +- **Partial updates:** Only provided fields are updated +- **Status handling:** Status updates trigger separate command internally +- **Progress updates:** Progress updates trigger separate command internally +- **Reassignment:** Changing `assignedPersonId` triggers reassignment logic + +--- + +### 6. Delete Action + +Soft delete an action. + +**Endpoint:** `DELETE /operations/actions/{actionId}` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `actionId` | string (GUID) | **Yes** | Action identifier | + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "deletedActionId": "action-123", + "deletedAt": "2025-12-23T17:30:00Z" + }, + "error": null +} +``` + +#### Business Rules + +- **Soft delete:** Action is marked as deleted but preserved in database +- **Relationships:** Connections to goals/strategies/issues are also soft deleted +- **Historical data:** All historical tracking is preserved + +--- + +### 7. Link Action to Goals + +Link an action to one or more goals. + +**Endpoint:** `PUT /operations/actions/{actionId}/goals` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `actionId` | string (GUID) | **Yes** | Action identifier | + +#### Request Body + +```json +{ + "goalIds": ["goal-001", "goal-002"] +} +``` + +#### Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `goalIds` | string[] (GUID) | **Yes** | Goal IDs to link (replaces existing links) | + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "id": "action-123", + "connections": { + "goalIds": ["goal-001", "goal-002"], + "strategyIds": ["strategy-002"], + "issueIds": [] + } + }, + "error": null +} +``` + +#### Business Rules + +- **Replace behavior:** Replaces existing goal links (not additive) +- **Multi-goal:** Actions can be linked to multiple goals +- **Validation:** All goal IDs must exist in tenant + +--- + +### 8. Link Action to Strategies + +Link an action to one or more strategies. + +**Endpoint:** `PUT /operations/actions/{actionId}/strategies` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `actionId` | string (GUID) | **Yes** | Action identifier | + +#### Request Body + +```json +{ + "strategyIds": ["strategy-001", "strategy-002"] +} +``` + +#### Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `strategyIds` | string[] (GUID) | **Yes** | Strategy IDs to link (replaces existing links) | + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "id": "action-123", + "connections": { + "goalIds": ["goal-001"], + "strategyIds": ["strategy-001", "strategy-002"], + "issueIds": [] + } + }, + "error": null +} +``` + +#### Business Rules + +- **Replace behavior:** Replaces existing strategy links (not additive) +- **Multi-strategy:** Actions can be linked to multiple strategies + +--- + +### 9. Link Action to Issues + +Link an action to one or more issues. + +**Endpoint:** `PUT /operations/actions/{actionId}/issues` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `actionId` | string (GUID) | **Yes** | Action identifier | + +#### Request Body + +```json +{ + "issueIds": ["issue-001", "issue-002"] +} +``` + +#### Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `issueIds` | string[] (GUID) | **Yes** | Issue IDs to link (replaces existing links, empty array clears links) | + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "id": "action-123", + "connections": { + "goalIds": ["goal-001"], + "strategyIds": ["strategy-002"], + "issueIds": ["issue-001", "issue-002"] + } + }, + "error": null +} +``` + +#### Business Rules + +- **Replace behavior:** Replaces existing issue links (not additive) +- **Multi-issue:** Actions can be linked to multiple issues +- **Empty array support:** Passing an empty array (`[]`) clears all issue connections +- **Validation:** All issue IDs must be valid GUIDs + +--- + +### 10. Remove All Relationships + +Remove all connections (goals, strategies, issues) from an action. + +**Endpoint:** `DELETE /operations/actions/{actionId}/relationships` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `actionId` | string (GUID) | **Yes** | Action identifier | + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "id": "action-123", + "connections": { + "goalIds": [], + "strategyIds": [], + "issueIds": [] + } + }, + "error": null +} +``` + +#### Business Rules + +- **Complete removal:** Removes all goal, strategy, and issue connections +- **Action preserved:** The action itself is not deleted, only relationships + +--- + +## Error Handling + +### Standard Error Response + +```json +{ + "success": false, + "data": null, + "error": "Error message here" +} +``` + +### Common Error Codes + +| Code | Scenario | Message Example | +|------|----------|-----------------| +| 400 | Invalid GUID format | "Invalid actionId format 'abc'. Must be a valid GUID." | +| 400 | Invalid enum value | "Invalid status value 'pending'. Allowed values: not_started, in_progress, completed, blocked, cancelled" | +| 400 | Invalid date format | "Invalid dueDate format '12/31/2025'. Must be a valid ISO 8601 date." | +| 400 | Date logic error | "DueDate must be after StartDate" | +| 401 | Missing/invalid token | "Unauthorized" | +| 403 | Insufficient permissions | "Access denied to this action" | +| 404 | Action not found | "Action not found" | +| 422 | Validation failure | "Title cannot be empty" | +| 500 | Server error | "Internal server error" | + +--- + +## Frontend Usage Examples + +### TypeScript Service + +```typescript +import { traction } from './traction'; + +// List actions with filters +const actions = await traction.get('/operations/actions', { + params: { + status: 'in_progress', + priority: 'high', + assignedPersonId: 'person-123', + page: 1, + limit: 20 + } +}); + +// Create action with connections +const newAction = await traction.post('/operations/actions', { + title: 'Launch new feature', + description: 'Complete feature development and QA', + startDate: '2025-12-20T00:00:00Z', + dueDate: '2025-12-31T00:00:00Z', + priority: 'high', + assignedPersonId: 'person-123', + estimatedHours: 40, + tags: ['feature', 'Q4'], + connections: { + goalIds: ['goal-001'], + strategyIds: ['strategy-002'] + } +}); + +// Update action status +await traction.put(`/operations/actions/${actionId}/status`, { + status: 'completed', + reason: 'All tasks completed', + actualHours: 38 +}); + +// Update action details +await traction.put(`/operations/actions/${actionId}`, { + title: 'Updated title', + dueDate: '2026-01-15T00:00:00Z', + progress: 80 +}); + +// Link to goals +await traction.put(`/operations/actions/${actionId}/goals`, { + goalIds: ['goal-001', 'goal-002'] +}); + +// Link to issues +await traction.put(`/operations/actions/${actionId}/issues`, { + issueIds: ['issue-001', 'issue-002'] +}); + +// Clear issue links (empty array) +await traction.put(`/operations/actions/${actionId}/issues`, { + issueIds: [] +}); + +// Delete action +await traction.delete(`/operations/actions/${actionId}`); +``` + +--- + +## Related APIs + +- **[Goals API](./goals-api.md)** - Manage goals that actions support +- **[Strategies API](./strategies-api.md)** - Strategies that actions implement +- **[Issues API](./issues-api.md)** - Issues that actions resolve +- **[People API](./people-api.md)** - People assigned to actions + +--- + +## Changelog + +### v7.2 (February 9, 2026) +- ✅ Added `search` query parameter to List Actions endpoint for searching title, description, and tags +- ✅ Added `startDate` as a sort option (previously supported in backend but not exposed in API) +- 📝 Enhanced filtering capabilities with case-insensitive substring search +- 📝 Note: Search by assignee name not supported; use `assignedPersonId` filter to search by specific assignee + +### v7.1 (February 6, 2026) +- ✅ Added endpoint #9: `PUT /operations/actions/{actionId}/issues` - Link action to issues +- 📝 Added support for empty array to clear issue connections +- 📝 Updated endpoint count from 9 to 10 endpoints +- 📝 Renumbered "Remove All Relationships" from #9 to #10 + +### v7.0 (December 23, 2025) +- ✅ Documented all 9 endpoints with complete examples +- 📝 Complete request/response structures for all operations +- 📝 Advanced filtering with multiple parameters +- 📝 Status and priority enums with descriptions +- 📝 Connection/relationship management endpoints +- 📝 Business rules and validation constraints +- 📝 Frontend TypeScript usage examples +- 📝 Error handling with specific codes + +### v6.0 (Previous) +- Initial action management endpoints + +--- + +**[← Back to Traction Service Index](./README.md)** diff --git a/docs/shared/Specifications/user-app/traction-service/alignment-api.md b/docs/shared/Specifications/user-app/traction-service/alignment-api.md new file mode 100644 index 00000000..f49c1435 --- /dev/null +++ b/docs/shared/Specifications/user-app/traction-service/alignment-api.md @@ -0,0 +1,244 @@ +# Alignment API Specification + +**Controller:** `AlignmentController` +**Base Route:** `/alignment` +**Version:** 7.2 +**Last Updated:** January 8, 2026 + +[← Back to API Index](./README.md) + +--- + +## Overview + +The Alignment API provides RESTful endpoints for saving and retrieving goal alignment data. + +**Important:** Alignment is calculated by the Coaching service (`POST /coaching/alignment-check`). This service provides endpoints to: +1. Save alignment data to Goals (`POST /alignment/{goalId}`) +2. Retrieve cached alignment data (`GET /alignment/{goalId}`) + +--- + +## Authentication + +All endpoints require: +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +--- + +## Endpoints + +### 1. Get Alignment Data + +**GET** `/alignment/{goalId}` + +Retrieve cached alignment data for a specific goal. + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `goalId` | string (UUID) | Yes | Goal ID to retrieve alignment for | + +#### Response 200 (Success) + +```json +{ + "success": true, + "data": { + "goalId": "db1f3932-108d-46e8-bb2f-4ec3e9366a66", + "alignmentScore": 85, + "explanation": "This goal strongly aligns with your business vision and purpose...", + "suggestions": [ + "Consider adding a strategy focused on customer acquisition", + "Your measures could be more specific about timeline" + ], + "componentScores": { + "intentAlignment": 90, + "strategyAlignment": 80, + "measureRelevance": 85 + }, + "breakdown": { + "visionAlignment": 88, + "purposeAlignment": 85, + "valuesAlignment": 82 + }, + "lastUpdated": "2025-10-13T14:30:00Z" + }, + "message": null, + "error": null, + "code": null +} +``` + +#### Response Fields + +| Field | Type | Description | +|------|------|-------------| +| `goalId` | string (UUID) | Goal ID | +| `alignmentScore` | integer | Overall alignment score (0-100) | +| `explanation` | string | AI-generated explanation of the alignment | +| `suggestions` | string[] | AI-generated suggestions for improvement | +| `componentScores` | object? | Detailed component scores (nullable) | +| `componentScores.intentAlignment` | integer | Intent alignment score (0-100) | +| `componentScores.strategyAlignment` | integer | Strategy alignment score (0-100) | +| `componentScores.measureRelevance` | integer | Measure relevance score (0-100) | +| `breakdown` | object? | Breakdown by business foundation elements (nullable) | +| `breakdown.visionAlignment` | integer | Vision alignment score (0-100) | +| `breakdown.purposeAlignment` | integer | Purpose alignment score (0-100) | +| `breakdown.valuesAlignment` | integer | Values alignment score (0-100) | +| `lastUpdated` | datetime | When alignment data was last updated | + +#### Response 404 (Goal Not Found) + +```json +{ + "success": false, + "data": null, + "message": null, + "error": "Goal not found", + "code": null +} +``` + +#### Response 404 (No Alignment Data) + +```json +{ + "success": false, + "data": null, + "message": null, + "error": "No alignment data found for this goal. Please calculate alignment using the Coaching service first.", + "code": null +} +``` + +#### Response 500 (Server Error) + +```json +{ + "success": false, + "data": null, + "message": null, + "error": "Internal server error", + "code": null +} +``` + +--- + +### 2. Save Alignment Data + +**POST** `/alignment/{goalId}` + +Save alignment data calculated by the Coaching service to a specific goal. + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `goalId` | string (UUID) | Yes | Goal ID to update with alignment data | + +#### Request Body + +```json +{ + "alignmentScore": 85, + "explanation": "This goal strongly aligns with your business vision and purpose...", + "suggestions": [ + "Consider adding a strategy focused on customer acquisition", + "Your measures could be more specific about timeline" + ], + "componentScores": { + "intentAlignment": 90, + "strategyAlignment": 80, + "measureRelevance": 85 + }, + "breakdown": { + "visionAlignment": 88, + "purposeAlignment": 85, + "valuesAlignment": 82 + } +} +``` + +#### Request Constraints + +| Field | Type | Required | Description | +|------|------|----------|-------------| +| `alignmentScore` | integer | Yes | Overall alignment score (0-100) | +| `explanation` | string | No | AI-generated explanation of the alignment (max 5000 chars) | +| `suggestions` | string[] | No | AI-generated suggestions for improvement (max 10 items) | +| `componentScores` | object | No | Detailed component scores | +| `componentScores.intentAlignment` | integer | Yes* | Intent alignment score (0-100) | +| `componentScores.strategyAlignment` | integer | Yes* | Strategy alignment score (0-100) | +| `componentScores.measureRelevance` | integer | Yes* | Measure relevance score (0-100) | +| `breakdown` | object | No | Breakdown by business foundation elements | +| `breakdown.visionAlignment` | integer | Yes* | Vision alignment score (0-100) | +| `breakdown.purposeAlignment` | integer | Yes* | Purpose alignment score (0-100) | +| `breakdown.valuesAlignment` | integer | Yes* | Values alignment score (0-100) | + +*Required if parent object is provided + +#### Response 200 (Success) + +```json +{ + "success": true, + "data": { + "goalId": "db1f3932-108d-46e8-bb2f-4ec3e9366a66", + "alignmentScore": 85, + "message": "Alignment data saved successfully" + }, + "message": null, + "error": null, + "code": null +} +``` + +#### Response 400 (Validation Error) + +```json +{ + "success": false, + "data": null, + "message": null, + "error": "Alignment score must be between 0 and 100", + "code": null +} +``` + +#### Response 404 (Goal Not Found) + +```json +{ + "success": false, + "data": null, + "message": null, + "error": "Goal not found", + "code": null +} +``` + +#### Response 500 (Server Error) + +```json +{ + "success": false, + "data": null, + "message": null, + "error": "Internal server error", + "code": null +} +``` + +--- + +## Notes + +- **RESTful Design**: Endpoints now follow RESTful conventions with resource IDs in URL paths +- **Frontend Context**: Frontend always has `goalId` from goal creation/selection, eliminating need for intent-based lookups +- **Performance**: Direct ID-based lookups are significantly faster than scanning and matching by intent string +- **Simplicity**: No unnecessary business foundation payload in GET requests +- **Breaking Change**: v7.2 removes deprecated `POST /alignment/check` and `POST /alignment/save` endpoints diff --git a/docs/shared/Specifications/user-app/traction-service/dashboard-reports-activities-api.md b/docs/shared/Specifications/user-app/traction-service/dashboard-reports-activities-api.md new file mode 100644 index 00000000..109474b9 --- /dev/null +++ b/docs/shared/Specifications/user-app/traction-service/dashboard-reports-activities-api.md @@ -0,0 +1,848 @@ +# Dashboard, Reports & Activities APIs + +**Service:** Traction Service +**Version:** v7.0 +**Last Updated:** December 23, 2025 + +## Overview + +This document covers three supporting API groups that provide aggregated views, reporting, and activity tracking across the Traction service: + +- **Dashboard API**: Command center with real-time alerts and summaries +- **Reports API**: Generate and retrieve PDF/DOCX reports +- **Activities API**: Activity feed tracking changes across entities + +--- + +## Table of Contents + +- [Dashboard API](#dashboard-api) + - [GET /dashboard/command-center](#get-command-center-dashboard) +- [Reports API](#reports-api) + - [GET /reports/company](#generate-company-report) + - [POST /reports](#create-custom-report) + - [GET /reports/{id}](#get-report) +- [Activities API](#activities-api) + - [GET /activity/recent](#get-recent-activities) + +--- + +# Dashboard API + +**Base Path:** `/dashboard` + +Provides aggregated views and real-time metrics for command center dashboards. + +## Get Command Center Dashboard + +**GET** `/dashboard/command-center` + +Retrieve aggregated command center dashboard with alerts, goals, tasks, and summary statistics. + +### Query Parameters + +```typescript +interface CommandCenterParams { + userId?: string; // Optional user ID (defaults to authenticated user) + daysAhead?: number; // Task window in days (default: 7) + varianceThreshold?: number; // Measure variance % to trigger "at risk" (default: 10) +} +``` + +### Response Structure + +```typescript +interface CommandCenterDashboardResponse { + alerts: { + measuresAtRisk: Array<{ + measureLinkId: string; + measureId: string; + measureName: string; + currentValue: number | null; + targetValue: number | null; + variance: number | null; + variancePercentage: number | null; + goalId?: string; + goalTitle?: string; + strategyId?: string; + strategyTitle?: string; + ownerId: string; + ownerName: string; + }>; + + actionsPastDue: Array<{ + actionId: string; + title: string; + dueDate: string; // ISO 8601 + daysPastDue: number; + priority: string; + assignedPersonId: string; + assignedPersonName: string; + goalId?: string; + goalTitle?: string; + strategyId?: string; + strategyTitle?: string; + }>; + + criticalIssues: Array<{ + issueId: string; + title: string; + impact: string; + priority: string; + reporterId: string; + reporterName: string; + dateReported: string; // ISO 8601 + statusName: string; + }>; + }; + + goals: Array<{ + id: string; + title: string; + intent: string; + status: string; + progress: number; // 0-100 + ownerId: string; + ownerName: string; + + strategies: Array<{ + id: string; + title: string; + progress: number; // 0-100 + atRisk: boolean; + }>; + + primaryMeasure?: { + measureId: string; + name: string; + currentValue: number | null; + targetValue: number | null; + progress: number; // 0-100 + }; + + stats: { + totalStrategies: number; + totalMeasures: number; + totalActions: number; + activeActions: number; + completedActions: number; + atRiskCount: number; + }; + }>; + + myTasks: Array<{ + id: string; + title: string; + description?: string; + dueDate: string; // ISO 8601 + priority: string; + status: string; + progress: number; // 0-100 + goalId?: string; + goalTitle?: string; + strategyId?: string; + strategyTitle?: string; + }>; + + summaryStats: { + totalActiveGoals: number; + goalsOnTrack: number; + goalsAtRisk: number; + goalsBehind: number; + totalMeasures: number; + measuresOnTrack: number; + measuresAtRisk: number; + measuresBehind: number; + totalActiveActions: number; + actionsPastDue: number; + actionsThisWeek: number; + openIssues: number; + criticalIssues: number; + }; + + generatedAt: string; // ISO 8601 +} +``` + +### Example Request + +```bash +GET /api/dashboard/command-center?daysAhead=14&varianceThreshold=15 +Authorization: Bearer {token} +X-Tenant-Id: {tenantId} +``` + +### Example Response + +```json +{ + "success": true, + "data": { + "alerts": { + "measuresAtRisk": [ + { + "measureLinkId": "link_abc123", + "measureId": "measure_revenue", + "measureName": "Monthly Recurring Revenue", + "currentValue": 85000, + "targetValue": 100000, + "variance": -15000, + "variancePercentage": -15.0, + "goalId": "goal_growth_2025", + "goalTitle": "Achieve $1.2M ARR", + "ownerId": "user_cfo", + "ownerName": "Jane Smith" + } + ], + "actionsPastDue": [ + { + "actionId": "action_urgent123", + "title": "Complete Q4 budget review", + "dueDate": "2025-12-15T17:00:00Z", + "daysPastDue": 8, + "priority": "high", + "assignedPersonId": "user_pm", + "assignedPersonName": "John Doe", + "goalId": "goal_operational", + "goalTitle": "Operational Excellence" + } + ], + "criticalIssues": [ + { + "issueId": "issue_prod123", + "title": "Production database timeout", + "impact": "critical", + "priority": "critical", + "reporterId": "user_eng", + "reporterName": "Alice Chen", + "dateReported": "2025-12-22T09:30:00Z", + "statusName": "In Progress" + } + ] + }, + + "goals": [ + { + "id": "goal_growth_2025", + "title": "Achieve $1.2M ARR", + "intent": "Drive sustainable revenue growth", + "status": "active", + "progress": 75.0, + "ownerId": "user_ceo", + "ownerName": "CEO Name", + + "strategies": [ + { + "id": "strat_marketing", + "title": "Expand marketing reach", + "progress": 65.0, + "atRisk": true + }, + { + "id": "strat_sales", + "title": "Increase sales conversion", + "progress": 85.0, + "atRisk": false + } + ], + + "primaryMeasure": { + "measureId": "measure_revenue", + "name": "Monthly Recurring Revenue", + "currentValue": 85000, + "targetValue": 100000, + "progress": 85.0 + }, + + "stats": { + "totalStrategies": 2, + "totalMeasures": 5, + "totalActions": 12, + "activeActions": 8, + "completedActions": 4, + "atRiskCount": 1 + } + } + ], + + "myTasks": [ + { + "id": "action_mytask1", + "title": "Review marketing campaign results", + "description": "Analyze Q4 campaign ROI", + "dueDate": "2025-12-26T17:00:00Z", + "priority": "high", + "status": "in_progress", + "progress": 60.0, + "goalId": "goal_growth_2025", + "goalTitle": "Achieve $1.2M ARR", + "strategyId": "strat_marketing", + "strategyTitle": "Expand marketing reach" + } + ], + + "summaryStats": { + "totalActiveGoals": 5, + "goalsOnTrack": 3, + "goalsAtRisk": 1, + "goalsBehind": 1, + "totalMeasures": 18, + "measuresOnTrack": 12, + "measuresAtRisk": 4, + "measuresBehind": 2, + "totalActiveActions": 34, + "actionsPastDue": 3, + "actionsThisWeek": 12, + "openIssues": 8, + "criticalIssues": 2 + }, + + "generatedAt": "2025-12-23T10:30:00Z" + } +} +``` + +### Business Rules + +- **Default User**: If `userId` not provided, uses authenticated user +- **At-Risk Measures**: Measures with `variance >= varianceThreshold` +- **Past Due Actions**: Actions with `dueDate < now` and status != completed +- **Critical Issues**: Issues with impact = "critical" or "high" +- **Task Window**: `myTasks` includes actions due within next `daysAhead` days + +### Use Cases + +- **Command Center UI**: Real-time dashboard for executives +- **Alert Notifications**: Trigger notifications for at-risk items +- **Team Standups**: Quick status overview for daily meetings +- **Mobile Dashboard**: Optimized summary view for mobile apps + +--- + +# Reports API + +**Base Path:** `/reports` + +Generate and retrieve PDF/DOCX reports for goals, Measures, and company-wide analytics. + +## Generate Company Report + +**GET** `/reports/company` + +Generate a company-wide PDF or DOCX report with goals, Measures, actions, and analytics. + +### Query Parameters + +```typescript +interface GenerateCompanyReportParams { + startDate?: string; // ISO 8601 date (default: 30 days ago) + endDate?: string; // ISO 8601 date (default: today) + format?: 'PDF' | 'DOCX'; // Default: 'PDF' (DOCX not yet supported) + includeAnalytics?: boolean; // Include analytics charts (default: true) + includeCharts?: boolean; // Include visual charts (default: true) + sections?: string[]; // Sections to include (default: all) +} +``` + +### Available Sections + +- `goals` - Goals summary and progress +- `measures` - Measure performance and trends +- `actions` - Action completion rates +- `issues` - Issue resolution statistics +- `people` - Team performance metrics +- `executive-summary` - High-level overview + +### Response + +Returns binary file (PDF or DOCX) with appropriate content-type header. + +**Success (200 OK)** +- Content-Type: `application/pdf` or `application/vnd.openxmlformats-officedocument.wordprocessingml.document` +- Content-Disposition: `attachment; filename="company-report-{timestamp}.pdf"` + +### Example Request + +```bash +GET /api/reports/company?startDate=2025-11-01&endDate=2025-12-23&format=PDF&includeAnalytics=true§ions=goals,measures,executive-summary +Authorization: Bearer {token} +X-Tenant-Id: {tenantId} +``` + +### Business Rules + +- **PDF Only**: DOCX generation returns 501 Not Implemented +- **Date Range**: Default is last 30 days +- **Section Filtering**: If `sections` provided, only those sections included +- **Charts**: Requires `includeCharts=true` and sufficient data +- **File Naming**: `company-report-{yyyyMMdd-HHmmss}.pdf` + +### Error Responses + +**400 Bad Request** +```json +{ + "success": false, + "error": "Invalid date range: startDate must be before endDate" +} +``` + +**501 Not Implemented** +```json +{ + "success": false, + "error": "DOCX format is not yet supported. Please use PDF format." +} +``` + +--- + +## Create Custom Report + +**POST** `/reports` + +Create a custom report configuration (asynchronous generation). + +### Request Body + +```typescript +interface CreateReportRequest { + title: string; // Report title + description?: string; // Optional description + reportType: string; // "Company" | "Goals" | "Measures" | "Actions" | "Custom" + startDate?: string; // ISO 8601 date + endDate?: string; // ISO 8601 date + filters?: Record; // Custom filters + sections?: string[]; // Sections to include +} +``` + +### Example Request + +```json +{ + "title": "Q4 2025 Measure Performance Report", + "description": "Detailed analysis of Q4 Measure trends", + "reportType": "Measures", + "startDate": "2025-10-01", + "endDate": "2025-12-31", + "sections": ["measures", "analytics", "trends"] +} +``` + +### Response + +**Success (201 Created)** +```json +{ + "success": true, + "data": { + "id": "report_abc123", + "title": "Q4 2025 Measure Performance Report", + "description": "Detailed analysis of Q4 Measure trends", + "status": "Pending", + "reportType": "Measures", + "startDate": "2025-10-01T00:00:00Z", + "endDate": "2025-12-31T23:59:59Z", + "createdAt": "2025-12-23T10:30:00Z", + "generatedAt": null, + "format": "PDF" + } +} +``` + +### Business Rules + +- **Async Generation**: Report status is "Pending" initially +- **Status Polling**: Use GET /reports/{id} to check status +- **Ready Status**: When status = "Ready", `downloadUrl` is available + +--- + +## Get Report + +**GET** `/reports/{id}` + +Retrieve report details and download URL. + +### Path Parameters + +- `id` (string, required): Report identifier (GUID) + +### Response + +**Success (200 OK)** +```json +{ + "success": true, + "data": { + "id": "report_abc123", + "title": "Q4 2025 Measure Performance Report", + "status": "Ready", + "reportType": "Measures", + "startDate": "2025-10-01T00:00:00Z", + "endDate": "2025-12-31T23:59:59Z", + "createdAt": "2025-12-23T10:30:00Z", + "generatedAt": "2025-12-23T10:35:00Z", + "format": "PDF", + "downloadUrl": "/api/v1/reports/company?startDate=2025-10-01&endDate=2025-12-31" + } +} +``` + +### Report Status Values + +- `Pending` - Report generation queued +- `Processing` - Report generation in progress +- `Ready` - Report available for download +- `Failed` - Report generation failed +- `Expired` - Report download link expired (after 7 days) + +--- + +# Activities API + +**Base Path:** `/activity` + +Track and retrieve activity feed across goals, strategies, actions, and issues. + +## Get Recent Activities + +**GET** `/activity/recent` + +Retrieve paginated recent activities across all goals for the tenant. + +### Query Parameters + +```typescript +interface RecentActivitiesParams { + page?: number; // Page number (default: 1) + size?: number; // Items per page (default: 10, max: 100) + type?: 'decision' | 'note' | 'attachment' | 'reading'; // Optional filter +} +``` + +### Response Structure + +```typescript +interface RecentActivitiesResponse { + success: boolean; + data: Array<{ + id: string; + type: 'decision' | 'note' | 'attachment' | 'reading'; + userId: string; + userName: string; + entityId?: string; // Goal/Strategy/Action/Issue ID + entityType?: 'goal' | 'strategy' | 'action' | 'issue'; + entityTitle?: string; + description: string; + timestamp: string; // ISO 8601 + }>; + pagination: { + page: number; + limit: number; + total: number; + totalPages: number; + }; +} +``` + +### Example Request + +```bash +GET /api/activity/recent?page=1&size=20&type=decision +Authorization: Bearer {token} +X-Tenant-Id: {tenantId} +``` + +### Example Response + +```json +{ + "success": true, + "data": [ + { + "id": "activity_abc123", + "type": "decision", + "userId": "user_ceo", + "userName": "Jane Smith", + "entityId": "goal_growth_2025", + "entityType": "goal", + "entityTitle": "Achieve $1.2M ARR", + "description": "Decided to focus on enterprise customers in Q1 2026", + "timestamp": "2025-12-23T09:15:00Z" + }, + { + "id": "activity_def456", + "type": "note", + "userId": "user_pm", + "userName": "John Doe", + "entityId": "strat_marketing", + "entityType": "strategy", + "entityTitle": "Expand marketing reach", + "description": "Updated campaign budget allocation", + "timestamp": "2025-12-23T08:45:00Z" + }, + { + "id": "activity_ghi789", + "type": "attachment", + "userId": "user_analyst", + "userName": "Alice Chen", + "entityId": "measure_revenue", + "entityType": "measure", + "entityTitle": "Monthly Recurring Revenue", + "description": "Uploaded Q4 revenue analysis spreadsheet", + "timestamp": "2025-12-22T16:30:00Z" + } + ], + "pagination": { + "page": 1, + "limit": 20, + "total": 147, + "totalPages": 8 + } +} +``` + +### Activity Types + +| Type | Description | +|------|-------------| +| `decision` | Strategic decision made | +| `note` | Comment or note added | +| `attachment` | File or document attached | +| `reading` | Weekly/monthly review reading | + +### Business Rules + +- **Pagination**: Default 10 items per page, max 100 +- **Tenant Scoped**: Only activities for current tenant +- **Chronological**: Sorted by timestamp descending (newest first) +- **Empty Response**: Returns 200 with empty array if no activities + +### Error Responses + +**400 Bad Request** +```json +{ + "success": false, + "error": "Invalid activity type: invalid_type" +} +``` + +--- + +## TypeScript Usage Examples + +### Dashboard Command Center + +```typescript +import axios from 'axios'; + +async function getCommandCenter( + daysAhead: number = 7, + varianceThreshold: number = 10 +) { + const response = await axios.get('/api/dashboard/command-center', { + params: { daysAhead, varianceThreshold }, + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'X-Tenant-Id': tenantId + } + }); + + const { alerts, summaryStats, myTasks } = response.data.data; + + console.log(`Critical Alerts: ${alerts.criticalIssues.length}`); + console.log(`Measures At Risk: ${alerts.measuresAtRisk.length}`); + console.log(`Past Due Actions: ${alerts.actionsPastDue.length}`); + console.log(`My Tasks This Week: ${myTasks.length}`); + + return response.data.data; +} + +// Dashboard refresh every 5 minutes +setInterval(async () => { + const dashboard = await getCommandCenter(14, 15); + updateDashboardUI(dashboard); +}, 5 * 60 * 1000); +``` + +### Generate and Download Report + +```typescript +async function generateCompanyReport( + startDate: string, + endDate: string, + sections: string[] = ['goals', 'measures', 'executive-summary'] +) { + const response = await axios.get('/api/reports/company', { + params: { + startDate, + endDate, + format: 'PDF', + includeAnalytics: true, + includeCharts: true, + sections: sections.join(',') + }, + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'X-Tenant-Id': tenantId + }, + responseType: 'blob' // Important for binary data + }); + + // Create download link + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `company-report-${Date.now()}.pdf`); + document.body.appendChild(link); + link.click(); + link.remove(); +} + +// Generate Q4 report +await generateCompanyReport('2025-10-01', '2025-12-31'); +``` + +### Activity Feed with Infinite Scroll + +```typescript +async function loadActivities( + page: number = 1, + size: number = 20, + type?: 'decision' | 'note' | 'attachment' | 'reading' +) { + const response = await axios.get('/api/activity/recent', { + params: { page, size, type }, + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'X-Tenant-Id': tenantId + } + }); + + return response.data; +} + +// Infinite scroll implementation +class ActivityFeed { + private currentPage = 1; + private activities: any[] = []; + + async loadMore() { + const result = await loadActivities(this.currentPage, 20); + this.activities.push(...result.data); + this.currentPage++; + return result.pagination.page < result.pagination.totalPages; + } + + async filterByType(type: string) { + this.currentPage = 1; + this.activities = []; + const result = await loadActivities(1, 20, type); + this.activities = result.data; + } +} + +const feed = new ActivityFeed(); +await feed.loadMore(); // Load first 20 +// ... user scrolls down ... +await feed.loadMore(); // Load next 20 +``` + +### Create Async Report and Poll Status + +```typescript +async function createAndDownloadReport( + title: string, + reportType: string, + startDate: string, + endDate: string +) { + // Step 1: Create report + const createResponse = await axios.post('/api/reports', { + title, + reportType, + startDate, + endDate, + sections: ['goals', 'measures', 'analytics'] + }, { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'X-Tenant-Id': tenantId + } + }); + + const reportId = createResponse.data.data.id; + + // Step 2: Poll for completion + let status = 'Pending'; + let downloadUrl = null; + + while (status !== 'Ready' && status !== 'Failed') { + await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds + + const statusResponse = await axios.get(`/api/reports/${reportId}`, { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'X-Tenant-Id': tenantId + } + }); + + status = statusResponse.data.data.status; + downloadUrl = statusResponse.data.data.downloadUrl; + } + + if (status === 'Ready') { + console.log(`Report ready: ${downloadUrl}`); + window.open(downloadUrl, '_blank'); + } else { + console.error('Report generation failed'); + } +} + +// Usage +await createAndDownloadReport( + 'Q4 2025 Performance', + 'Company', + '2025-10-01', + '2025-12-31' +); +``` + +--- + +## Related APIs + +- **[Goals API](./goals-api.md)**: Goal data for dashboard and reports +- **[Measures API](./measures-api.md)**: Measure metrics for dashboard and reports +- **[Actions API](./actions-api.md)**: Action data for task tracking +- **[Issues API](./issues-api.md)**: Issue data for alerts + +--- + +## Changelog + +### v7.0 (December 23, 2025) +- **Dashboard**: Command center with real-time alerts, goals, tasks, summary stats +- **Reports**: Company report generation (PDF), async custom reports +- **Activities**: Paginated activity feed with type filtering + +--- + +## Known Limitations + +1. **DOCX Reports**: Not yet implemented (returns 501) +2. **Custom Report Storage**: Currently placeholder implementation +3. **Report Expiry**: Download links expire after 7 days (not yet enforced) +4. **Activity Real-time**: Polling-based, not using SSE/WebSockets yet + +--- + +## Support + +For questions or issues: +- **GitHub Issues**: [PurposePath Repository](https://github.com/purposepath/backend) +- **Slack**: #traction-service channel +- **Email**: backend-support@purposepath.com diff --git a/docs/shared/Specifications/user-app/traction-service/goals-api.md b/docs/shared/Specifications/user-app/traction-service/goals-api.md new file mode 100644 index 00000000..b30e209f --- /dev/null +++ b/docs/shared/Specifications/user-app/traction-service/goals-api.md @@ -0,0 +1,1119 @@ +# Goals API Specification + +**Controller:** `GoalsController` +**Base Route:** `/goals` +**Version:** 7.1 +**Last Updated:** December 27, 2025 + +[← Back to API Index](./README.md) + +--- + +## Overview + +The Goals API manages the complete lifecycle of business goals, including creation, updates, status transitions, strategies, and activity tracking. + +**Endpoints:** 14 total +- 3 CRUD operations (List, Create, Get, Update, Delete) +- 3 status transitions (Close, Activate, Pause) +- 2 activity endpoints (Get activity, Add activity) +- 1 notes endpoint +- 1 statistics endpoint +- 2 Measure endpoints (Set Measure threshold, Get available Measures) +- 2 strategy endpoints (Create strategy, Update strategy) + +--- + +## Authentication + +All endpoints require: +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +--- + +## Endpoints + +### 1. List Goals + +**GET** `/goals` + +Retrieve a filtered, paginated list of goals. + +#### Query Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `page` | number | No | 1 | Page number (1-indexed) | +| `pageSize` | number | No | 20 | Items per page (max 100) | +| `sortBy` | string | No | "createdAt" | Sort field: "name", "status", "startDate", "targetDate", "createdAt" | +| `sortOrder` | string | No | "desc" | Sort direction: "asc" or "desc" | +| `status` | string | No | - | Filter by status: "draft", "active", "paused", "completed", "cancelled" | +| `personId` | string (UUID) | No | - | Filter by assigned person | +| `type` | string | No | - | Filter by goal type | +| `searchTerm` | string | No | - | Search in goal name and description | + +#### Response 200 + +```json +{ + "success": true, + "data": { + "items": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "Increase Revenue by 25%", + "description": "Achieve 25% revenue growth through new markets", + "status": "active", + "type": "annual", + "targetDate": "2025-12-31T23:59:59.999Z", + "startDate": "2025-01-01T00:00:00.000Z", + "completionDate": null, + "progress": 42.5, + "owner": { + "id": "user-123", + "firstName": "John", + "lastName": "Doe", + "email": "john@example.com" + }, + "strategiesCount": 3, + "measuresCount": 5, + "actionsCount": 12, + "issuesCount": 2, + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-12-20T15:30:00.000Z" + } + ], + "pagination": { + "page": 1, + "pageSize": 20, + "total": 45, + "totalPages": 3 + } + }, + "error": null, + "timestamp": "2025-12-23T10:30:00.000Z" +} +``` + +--- + +### 2. Create Goal + +**POST** `/goals` + +Create a new goal. + +#### Request Body + +```json +{ + "name": "Increase Revenue by 25%", + "description": "Achieve 25% revenue growth through new markets and product expansion", + "type": "annual", + "targetDate": "2025-12-31T23:59:59.999Z", + "startDate": "2025-01-01T00:00:00.000Z", + "ownerId": "user-123", + "status": "draft" +} +``` + +#### Request Fields + +| Field | Type | Required | Constraints | Description | +|-------|------|----------|-------------|-------------| +| `name` | string | Yes | 1-200 chars | Goal name | +| `description` | string | No | Max 2000 chars | Detailed description | +| `type` | string | Yes | See enum below | Goal type/timeframe | +| `targetDate` | string (ISO 8601) | Yes | Future date | Target completion date | +| `startDate` | string (ISO 8601) | No | Defaults to today | Goal start date | +| `ownerId` | string (UUID) | Yes | Must exist | Responsible person ID | +| `status` | string | No | Default: "draft" | Initial status | + +**Type Enum:** +- `annual` - Year-long goal +- `quarterly` - 3-month goal +- `monthly` - 30-day goal +- `custom` - Custom timeframe + +**Status Enum:** +- `draft` - Being planned +- `active` - In progress +- `paused` - Temporarily stopped +- `completed` - Successfully finished +- `cancelled` - Abandoned + +#### Response 201 + +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "Increase Revenue by 25%", + "description": "Achieve 25% revenue growth through new markets and product expansion", + "status": "draft", + "type": "annual", + "targetDate": "2025-12-31T23:59:59.999Z", + "startDate": "2025-01-01T00:00:00.000Z", + "completionDate": null, + "progress": 0, + "owner": { + "id": "user-123", + "firstName": "John", + "lastName": "Doe", + "email": "john@example.com" + }, + "createdAt": "2025-12-23T10:30:00.000Z", + "updatedAt": "2025-12-23T10:30:00.000Z", + "createdBy": "user-123", + "tenantId": "tenant-456" + }, + "error": null, + "timestamp": "2025-12-23T10:30:00.000Z" +} +``` + +--- + +### 3. Get Goal Details + +**GET** `/goals/{id}` + +Retrieve detailed information about a specific goal, including strategies, Measures, and metrics. + +#### Path Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `id` | string (UUID) | Goal identifier | + +#### Response 200 + +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "Increase Revenue by 25%", + "description": "Achieve 25% revenue growth through new markets", + "status": "active", + "type": "annual", + "targetDate": "2025-12-31T23:59:59.999Z", + "startDate": "2025-01-01T00:00:00.000Z", + "completionDate": null, + "progress": 42.5, + "owner": { + "id": "user-123", + "firstName": "John", + "lastName": "Doe", + "email": "john@example.com", + "role": "CEO" + }, + "strategies": [ + { + "id": "strat-001", + "name": "Expand to APAC Markets", + "description": "Enter 5 new markets in Asia-Pacific region", + "order": 1, + "status": "active", + "progress": 60, + "measuresCount": 3, + "createdAt": "2025-01-15T00:00:00.000Z" + } + ], + "measures": [ + { + "measureLinkId": "link-001", + "strategyId": null, + "isPrimary": true, + "thresholdPct": 10.0, + "progressStatus": "on_track", + "measureId": "measure-001", + "name": "Monthly Recurring Revenue", + "unit": "USD", + "direction": "up", + "currentValue": 125000, + "targetValue": null, + "progress": 78.5, + "variance": -5.2, + "category": "revenue", + "catalogId": "catalog-mrr-001", + "aggregationType": "sum", + "aggregationPeriod": "monthly", + "type": "quantitative", + "ownerId": "user-456" + } + ], + "statistics": { + "strategiesCount": 3, + "measuresCount": 5, + "actionsCount": 12, + "openIssuesCount": 2, + "completedActionsCount": 8, + "overallHealth": "on_track" + }, + "recentActivity": [ + { + "id": "act-001", + "type": "measure_updated", + "description": "MRR updated to $125,000", + "createdAt": "2025-12-22T15:30:00.000Z", + "createdBy": { + "firstName": "Jane", + "lastName": "Smith" + } + } + ], + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-12-20T15:30:00.000Z", + "createdBy": "user-123", + "tenantId": "tenant-456" + }, + "error": null, + "timestamp": "2025-12-23T10:30:00.000Z" +} +``` + +#### Measure Response Fields + +The `measures` array contains enriched measure data combining both MeasureLink and Measure properties: + +| Field | Type | Nullable | Description | +|-------|------|----------|-------------| +| **MeasureLink Properties** | | | | +| `measureLinkId` | string (UUID) | No | Unique identifier for the measure-goal link | +| `strategyId` | string (UUID) | Yes | Strategy this measure is linked to (null for goal-level measures) | +| `isPrimary` | boolean | No | Whether this is the primary measure for the goal | +| `thresholdPct` | number | Yes | Variance threshold percentage for alerts (0-100) | +| `progressStatus` | string | Yes | Calculated status: "on_track", "at_risk", "behind" | +| **Measure Properties** | | | | +| `measureId` | string (UUID) | No | Unique identifier for the measure | +| `name` | string | No | Measure name | +| `unit` | string | No | Unit of measurement (e.g., "USD", "users", "%") | +| `direction` | string | No | Expected direction: "up", "down", "maintain" | +| `currentValue` | number | Yes | Current/latest measured value | +| `targetValue` | number | Yes | (Deprecated) Target value - now stored separately | +| `progress` | number | Yes | Progress percentage toward target (calculated separately) | +| `variance` | number | Yes | Difference from target (calculated separately) | +| `category` | string | Yes | Business category (e.g., "revenue", "growth", "efficiency") | +| `catalogId` | string (UUID) | Yes | Reference to measure catalog entry | +| `aggregationType` | string | Yes | How values aggregate: "sum", "average", "count", "min", "max", "latest", "point_in_time" | +| `aggregationPeriod` | string | Yes | Time period: "daily", "weekly", "monthly", "quarterly", "yearly" | +| `type` | string | No | Input type: "quantitative" (numeric), "qualitative" (options), "binary" (yes/no) | +| `ownerId` | string (UUID) | Yes | Measure owner - the person responsible for this measure | + +**Note:** `progress`, `variance`, and `progressStatus` are calculated from measure targets and actuals using the domain progress calculation service (same as the measures endpoint). `targetValue` remains `null` as targets are stored separately in the system. `progressStatus` reflects the calculated status relative to link thresholds ("on_track", "at_risk", "behind"). If insufficient data exists for a measure (less than 2 actuals or no target), these fields will be `null`. + +--- + +### 4. Update Goal + +**PUT** `/goals/{id}` + +Update an existing goal's properties. + +#### Path Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `id` | string (UUID) | Goal identifier | + +#### Request Body + +```json +{ + "name": "Increase Revenue by 30%", + "description": "Updated target to 30% growth", + "targetDate": "2025-12-31T23:59:59.999Z", + "ownerId": "user-456" +} +``` + +#### Request Fields + +All fields are optional (only include fields to update): + +| Field | Type | Constraints | Description | +|-------|------|-------------|-------------| +| `name` | string | 1-200 chars | Goal name | +| `description` | string | Max 2000 chars | Description | +| `type` | string | See enum | Goal type | +| `targetDate` | string (ISO 8601) | Future date | Target date | +| `startDate` | string (ISO 8601) | - | Start date | +| `ownerId` | string (UUID) | Must exist | Owner ID | + +**Note:** Status is updated via dedicated endpoints (activate, pause, close) + +#### Owner Change Propagation + +When the `ownerId` field is updated, the system automatically propagates the ownership change to related entities following the **unified measure ownership model**: + +**Propagation Logic:** +1. System identifies all Measures linked to this goal +2. For each Measure where `Measure.OwnerId` equals the goal's **previous owner**: + - Updates `Measure.OwnerId` to the new owner + - Fetches **ALL MeasureLinks** for that Measure (across all goals and strategies) + - Updates `MeasureLink.personId` to the new owner for every link + +**Example Scenario:** +``` +Initial State: +- Goal A (owner: Alice) +- Measure M1 (owner: Alice) linked to Goal A +- MeasureLinks: Link1→Goal A, Link2→Goal B, Link3→Strategy S1 (all with personId: Alice) + +After updating Goal A owner to Bob: +- Goal A (owner: Bob) +- Measure M1 (owner: Bob) - UPDATED because it matched old owner +- MeasureLinks: Link1, Link2, Link3 (all with personId: Bob) - ALL updated + +If Measure M2 had a different owner (Charlie), it would NOT be affected. +``` + +**Key Behaviors:** +- ✅ Only affects Measures that were owned by the previous goal owner +- ✅ Updates ALL links for affected Measures (maintains unified ownership) +- ✅ Preserves explicitly assigned owners (Measures not matching old owner remain unchanged) +- ✅ Handles multi-goal scenarios correctly (Measure linked to multiple goals) +- ✅ Operation is atomic and logged for audit purposes + +**Logging:** The operation logs: +- Number of Measures updated +- Number of MeasureLinks updated +- Individual Measure updates with owner transition details + +#### Response 200 + +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "Increase Revenue by 30%", + "description": "Updated target to 30% growth", + "status": "active", + "type": "annual", + "targetDate": "2025-12-31T23:59:59.999Z", + "startDate": "2025-01-01T00:00:00.000Z", + "progress": 42.5, + "owner": { + "id": "user-456", + "firstName": "Jane", + "lastName": "Smith" + }, + "updatedAt": "2025-12-23T10:35:00.000Z" + }, + "error": null, + "timestamp": "2025-12-23T10:35:00.000Z" +} +``` + +--- + +### 5. Delete Goal + +**DELETE** `/goals/{id}` + +Permanently delete a goal and all associated data (strategies, Measure links, activities). + +⚠️ **Warning:** This operation cannot be undone. Consider closing instead. + +#### Path Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `id` | string (UUID) | Goal identifier | + +#### Response 200 + +```json +{ + "success": true, + "data": { + "message": "Goal deleted successfully" + }, + "error": null, + "timestamp": "2025-12-23T10:40:00.000Z" +} +``` + +--- + +### 6. Close Goal + +**POST** `/goals/{id}:close` + +Close a goal marking it as completed or cancelled. + +#### Path Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `id` | string (UUID) | Goal identifier | + +#### Request Body + +```json +{ + "finalStatus": "completed", + "notes": "Successfully achieved 30% revenue growth" +} +``` + +#### Request Fields + +| Field | Type | Required | Constraints | Description | +|-------|------|----------|-------------|-------------| +| `finalStatus` | string | Yes | "completed" or "cancelled" | Final status | +| `notes` | string | No | Max 1000 chars | Closing notes | + +#### Response 200 + +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "status": "completed", + "completionDate": "2025-12-23T10:45:00.000Z", + "progress": 100, + "finalNotes": "Successfully achieved 30% revenue growth" + }, + "error": null, + "timestamp": "2025-12-23T10:45:00.000Z" +} +``` + +--- + +### 7. Activate Goal + +**POST** `/goals/{id}:activate` + +Activate a goal, transitioning from draft or paused status to active. + +#### Path Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `id` | string (UUID) | Goal identifier | + +#### Request Body + +```json +{} +``` + +No body required (empty object or omit). + +#### Response 200 + +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "status": "active", + "activatedAt": "2025-12-23T10:50:00.000Z" + }, + "error": null, + "timestamp": "2025-12-23T10:50:00.000Z" +} +``` + +--- + +### 8. Pause Goal + +**POST** `/goals/{id}:pause` + +Temporarily pause an active goal. + +#### Path Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `id` | string (UUID) | Goal identifier | + +#### Request Body + +```json +{ + "reason": "Waiting for budget approval" +} +``` + +#### Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `reason` | string | No | Reason for pausing | + +#### Response 200 + +```json +{ + "success": true, + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "status": "paused", + "pausedAt": "2025-12-23T10:55:00.000Z", + "pauseReason": "Waiting for budget approval" + }, + "error": null, + "timestamp": "2025-12-23T10:55:00.000Z" +} +``` + +--- + +### 9. Get Goal Activity + +**GET** `/goals/{id}/activity` + +Retrieve activity feed for a goal (updates, changes, comments). + +#### Path Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `id` | string (UUID) | Goal identifier | + +#### Query Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `page` | number | No | 1 | Page number | +| `pageSize` | number | No | 20 | Items per page | +| `types` | string | No | - | Comma-separated activity types to filter | + +**Activity Types:** +- `goal_created`, `goal_updated`, `goal_status_changed` +- `strategy_added`, `strategy_updated`, `strategy_removed` +- `measure_linked`, `measure_unlinked`, `measure_updated` +- `comment_added`, `note_added` + +#### Response 200 + +```json +{ + "success": true, + "data": { + "items": [ + { + "id": "activity-001", + "type": "measure_updated", + "entityType": "measure", + "entityId": "measure-001", + "description": "MRR updated from $120,000 to $125,000", + "metadata": { + "measureName": "Monthly Recurring Revenue", + "previousValue": 120000, + "newValue": 125000 + }, + "createdAt": "2025-12-22T15:30:00.000Z", + "createdBy": { + "id": "user-123", + "firstName": "John", + "lastName": "Doe" + } + } + ], + "pagination": { + "page": 1, + "pageSize": 20, + "total": 156, + "totalPages": 8 + } + }, + "error": null, + "timestamp": "2025-12-23T11:00:00.000Z" +} +``` + +--- + +### 10. Add Goal Activity + +**POST** `/goals/{id}/activity` + +Add a manual activity entry (comment, note, update). + +#### Path Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `id` | string (UUID) | Goal identifier | + +#### Request Body + +```json +{ + "type": "comment", + "content": "Updated strategy based on Q4 results", + "metadata": { + "relatedStrategyId": "strat-001" + } +} +``` + +#### Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `type` | string | Yes | Activity type: "comment", "note", "update" | +| `content` | string | Yes | Activity content (max 2000 chars) | +| `metadata` | object | No | Additional structured data | + +#### Response 201 + +```json +{ + "success": true, + "data": { + "id": "activity-002", + "type": "comment", + "content": "Updated strategy based on Q4 results", + "createdAt": "2025-12-23T11:05:00.000Z", + "createdBy": { + "id": "user-123", + "firstName": "John", + "lastName": "Doe" + } + }, + "error": null, + "timestamp": "2025-12-23T11:05:00.000Z" +} +``` + +--- + +### 11. Add Goal Note + +**POST** `/goals/{id}/notes` + +Add a note to a goal (similar to activity but specifically for notes). + +#### Path Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `id` | string (UUID) | Goal identifier | + +#### Request Body + +```json +{ + "content": "Meeting scheduled with stakeholders for Jan 15", + "category": "planning" +} +``` + +#### Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `content` | string | Yes | Note content (max 5000 chars) | +| `category` | string | No | Note category for organization | + +#### Response 201 + +```json +{ + "success": true, + "data": { + "id": "note-001", + "content": "Meeting scheduled with stakeholders for Jan 15", + "category": "planning", + "createdAt": "2025-12-23T11:10:00.000Z", + "createdBy": { + "id": "user-123", + "firstName": "John", + "lastName": "Doe" + } + }, + "error": null, + "timestamp": "2025-12-23T11:10:00.000Z" +} +``` + +--- + +### 12. Get Goal Statistics + +**GET** `/goals/stats` + +Get aggregated statistics across all goals for the tenant. + +#### Query Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `type` | string | No | Filter by goal type | +| `ownerId` | string (UUID) | No | Filter by owner | + +#### Response 200 + +```json +{ + "success": true, + "data": { + "totalGoals": 45, + "byStatus": { + "draft": 5, + "active": 30, + "paused": 3, + "completed": 5, + "cancelled": 2 + }, + "byType": { + "annual": 20, + "quarterly": 15, + "monthly": 10 + }, + "averageProgress": 52.3, + "onTrackCount": 25, + "atRiskCount": 8, + "offTrackCount": 2, + "totalMeasures": 125, + "totalStrategies": 89, + "totalActions": 456 + }, + "error": null, + "timestamp": "2025-12-23T11:15:00.000Z" +} +``` + +--- + +### 13. Set Measure Threshold + +**POST** `/goals/{goalId}/measures/{measureId}:setThreshold` + +Set a threshold percentage for a Measure linked to a goal. + +#### Path Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `goalId` | string (UUID) | Goal identifier | +| `measureId` | string (UUID) | Measure identifier | + +#### Request Body + +```json +{ + "thresholdPct": 85.0 +} +``` + +#### Request Fields + +| Field | Type | Required | Constraints | Description | +|-------|------|----------|-------------|-------------| +| `thresholdPct` | number | Yes | 0-100 | Threshold percentage | + +**Threshold Behavior:** +- Measure shows warning when progress falls below this percentage +- Used for status calculations (on_track, at_risk, off_track) +- Typical values: 80-90% + +#### Response 200 + +```json +{ + "success": true, + "data": { + "goalId": "550e8400-e29b-41d4-a716-446655440000", + "measureId": "measure-001", + "thresholdPct": 85.0, + "updatedAt": "2025-12-23T11:20:00.000Z" + }, + "error": null, + "timestamp": "2025-12-23T11:20:00.000Z" +} +``` + +--- + +### 14. Get Available Measures + +**GET** `/goals/{goalId}/available-measures` + +Retrieve all Measures available for linking to a specific goal, including: +- Catalog Measures (predefined industry-standard Measures) +- Tenant custom Measures (organization-specific Measures) + +Each Measure includes usage statistics showing how many goals are currently using it and whether the specified goal is already using it. + +#### Path Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `goalId` | string (UUID) | Goal identifier | + +#### Response 200 + +```json +{ + "success": true, + "data": { + "catalogMeasures": [ + { + "id": "catalog-001", + "name": "Monthly Recurring Revenue", + "description": "Total predictable revenue from subscriptions", + "category": "Financial", + "unit": "USD", + "direction": "increase", + "type": "leading", + "valueType": "currency", + "aggregationType": "sum", + "aggregationPeriod": "monthly", + "calculationMethod": "Sum of all active subscription values", + "isIntegrationEnabled": true, + "usageInfo": { + "goalCount": 3, + "isUsedByThisGoal": false + } + } + ], + "tenantCustomMeasures": [ + { + "id": "custom-measure-001", + "name": "Customer Satisfaction Score", + "description": "Average CSAT from post-purchase surveys", + "category": "Customer Experience", + "unit": "score", + "direction": "increase", + "type": "lagging", + "valueType": "percentage", + "aggregationType": "average", + "aggregationPeriod": "monthly", + "calculationMethod": "Average of all survey responses", + "measureCatalogId": null, + "isIntegrationEnabled": false, + "createdAt": "2025-01-15T10:00:00.000Z", + "createdBy": "user-123", + "usageInfo": { + "goalCount": 1, + "isUsedByThisGoal": true + } + } + ] + }, + "error": null, + "timestamp": "2025-12-23T11:30:00.000Z" +} +``` + +#### Response Fields + +**Catalog Measure:** +| Field | Type | Description | +|-------|------|-------------| +| `id` | string (UUID) | Measure catalog entry ID | +| `name` | string | Measure display name | +| `description` | string | What the Measure measures | +| `category` | string | Business category (Financial, Operations, Customer, etc.) | +| `unit` | string | Measurement unit (USD, %, count, score, etc.) | +| `direction` | string | `"increase"` or `"decrease"` - desired trend | +| `type` | string | `"leading"` or `"lagging"` indicator | +| `valueType` | string | `"number"`, `"currency"`, `"percentage"`, `"boolean"` | +| `aggregationType` | string | `"sum"`, `"average"`, `"max"`, `"min"`, `"count"`, `"latest"` | +| `aggregationPeriod` | string | `"daily"`, `"weekly"`, `"monthly"`, `"quarterly"`, `"annually"` | +| `calculationMethod` | string | How the Measure is calculated | +| `isIntegrationEnabled` | boolean | Whether external integrations can populate this Measure | +| `usageInfo` | object | Usage statistics for this Measure | + +**Tenant Custom Measure:** +| Field | Type | Description | +|-------|------|-------------| +| All fields from Catalog Measure | - | Same as above | +| `measureCatalogId` | string (UUID) or null | Reference to catalog Measure if based on template | +| `createdAt` | string (ISO 8601) | When the custom Measure was created | +| `createdBy` | string (UUID) | User who created the custom Measure | + +**Usage Info:** +| Field | Type | Description | +|-------|------|-------------| +| `goalCount` | number | Number of goals currently using this Measure | +| `isUsedByThisGoal` | boolean | Whether the specified goal is already using this Measure | + +#### Use Cases + +1. **Measure Selection Dialog**: Display available Measures when user wants to link Measure to goal +2. **Prevent Duplicates**: Show `isUsedByThisGoal` to prevent linking same Measure twice +3. **Popular Measures**: Sort by `goalCount` to show most commonly used Measures +4. **Filter by Category**: Group Measures by category for better UX + +#### Error Responses + +**404 Not Found** +```json +{ + "success": false, + "data": null, + "error": "Goal not found", + "timestamp": "2025-12-23T11:30:00.000Z" +} +``` + +**400 Bad Request** +```json +{ + "success": false, + "data": null, + "error": "Invalid goal ID format", + "timestamp": "2025-12-23T11:30:00.000Z" +} +``` + +#### Notes + +- Returns both catalog Measures (predefined) and tenant custom Measures +- Usage statistics updated in real-time +- Only returns Measures accessible to the tenant +- Catalog Measures are read-only; tenant custom Measures can be modified +- Use [Measure Links API](./measure-links-api.md) to actually link a Measure to the goal +- For new (unpersisted) goals, use `GET /goals/available-measures` (see Measures API) to fetch the catalog without a goalId + +--- + +## Error Responses + +### 400 Bad Request + +```json +{ + "success": false, + "data": null, + "error": "Invalid goal ID format", + "timestamp": "2025-12-23T11:25:00.000Z" +} +``` + +### 404 Not Found + +```json +{ + "success": false, + "data": null, + "error": "Goal not found", + "timestamp": "2025-12-23T11:25:00.000Z" +} +``` + +### 422 Unprocessable Entity + +```json +{ + "success": false, + "data": null, + "error": "Cannot activate goal that is already active", + "timestamp": "2025-12-23T11:25:00.000Z" +} +``` + +--- + +## Business Rules + +### Goal Status Transitions + +Valid transitions: +- `draft` → `active`, `cancelled` +- `active` → `paused`, `completed`, `cancelled` +- `paused` → `active`, `cancelled` +- `completed` - Terminal state (no transitions) +- `cancelled` - Terminal state (no transitions) + +### Deletion Rules + +- Cannot delete goal with active linked Measures +- Cannot delete goal with open issues +- Deleting goal also deletes: strategies, activity history, notes +- Deleting goal unlinks: Measures (Measure instances remain), actions, people + +### Validation Rules + +- Goal name: 1-200 characters, required +- Description: max 2000 characters +- Target date: must be in future +- Start date: must be before target date +- Owner: must be valid person in tenant +- Type: must be valid enum value + +--- + +## Examples + +### Frontend Usage (TypeScript) + +```typescript +import { traction } from '@/services/traction'; + +// List active goals +const { data } = await traction.get('/goals', { + params: { status: 'active', pageSize: 10 } +}); + +// Create new goal +const newGoal = await traction.post('/goals', { + name: 'Increase Revenue by 25%', + description: 'Achieve 25% revenue growth', + type: 'annual', + targetDate: '2025-12-31T23:59:59.999Z', + ownerId: currentUser.id, + status: 'draft' +}); + +// Activate goal +await traction.post(`/goals/${goalId}:activate`); + +// Close goal +await traction.post(`/goals/${goalId}:close`, { + finalStatus: 'completed', + notes: 'Successfully achieved target' +}); +``` + +--- + +## Related APIs + +- [Strategies API](./strategies-api.md) - Manage goal strategies +- [Measure Links API](./measure-links-api.md) - Link Measures to goals +- [Actions API](./actions-api.md) - Link actions to goals +- [Activities API](./activities-api.md) - Activity feeds + +--- + +## Changelog + +### v7.6 (Issue #645) +- **Enhanced** `/goals/{id}` endpoint to calculate measure progress using domain service +- `progress`, `variance`, and `progressStatus` fields in measures array now return actual calculated values (instead of `null`) +- Progress calculated using `IProgressCalculationService` for consistency with measures endpoint +- Handler pre-calculates progress for each measure link before passing to mapper +- Mapper remains pure transformation (no business logic) +- Uses new `MeasureLinkWithProgress` result record for type-safe, cohesive data structure + +### v7.1 (December 27, 2025) +- **Restored** `/goals/{goalId}/available-measures` endpoint (Issue #413) +- Updated to work with new `IMeasureLinkRepository` architecture +- Added comprehensive documentation with usage examples +- Endpoint now supports unified Measure linking for Goals, Strategies, and People + +### v7.0 (December 23, 2025) +- Removed deprecated `/goals/{goalId}/measures:link` endpoint +- Removed deprecated `/goals/{goalId}/measures:unlink` endpoint +- ~~Removed deprecated `/goals/{goalId}/available-measures` endpoint~~ (Restored in v7.1) +- Measure operations moved to dedicated `/measure-links` API +- Added comprehensive documentation with examples + +### v6.0 (December 21, 2025) +- Added Measure linking endpoints (now deprecated) +- Added activity endpoints +- Added statistics endpoint diff --git a/docs/shared/Specifications/user-app/traction-service/insights-api.md b/docs/shared/Specifications/user-app/traction-service/insights-api.md new file mode 100644 index 00000000..481f3126 --- /dev/null +++ b/docs/shared/Specifications/user-app/traction-service/insights-api.md @@ -0,0 +1,257 @@ +# Coaching Insights API + +**Service:** Traction Service (.NET) +**Controller:** InsightsController +**Version:** v1.0 +**Last Updated:** February 4, 2026 + +## Overview + +The Coaching Insights API manages AI-generated coaching insights and recommendations. Insights are generated by the Python Coaching Service and persisted in the Traction Service for retrieval and management. + +**Architecture Flow:** +1. Frontend calls Python Coaching Service `POST /insights/generate` (documented in [coaching-service.md](../coaching-service.md)) +2. Python AI returns insights with snake_case properties (kiss_category, alignment_impact, etc.) +3. Frontend persists to Traction Service `POST /traction/api/v1/insights/batch` (snake_case accepted via JsonPropertyName) +4. C# API returns camelCase properties (kissCategory, alignmentImpact, etc.) - standard .NET convention +5. Widget retrieves via dashboard endpoint (documented in [dashboard-service.md](../dashboard-service.md#ai-insights-widget)) + +--- + +## Table of Contents + +- [Save Coaching Insights Batch](#save-coaching-insights-batch) +- [Get All Coaching Insights](#get-all-coaching-insights) +- [Get Coaching Insight by ID](#get-coaching-insight-by-id) +- [Update Coaching Insight](#update-coaching-insight) +- [Delete Coaching Insight](#delete-coaching-insight) + +--- + +## Save Coaching Insights Batch + +**POST** `/traction/api/v1/insights/batch` + +Save multiple coaching insights after generation from Python AI service. + +### Request Body + +```json +{ + "insights": [ + { + "title": "Focus on Enterprise Segment", + "description": "Analysis shows 40% higher conversion rates in enterprise segment.", + "category": "strategy", + "priority": "high", + "kissCategory": "start", + "alignmentImpact": "This insight directly supports our growth objectives.", + "status": "active", + "businessImpact": "high", + "effortRequired": "medium", + "createdAt": "2026-02-02T23:09:51.327Z" + } + ] +} +``` + +### Request Field Definitions + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `insights` | array | Yes | Array of coaching insight objects | +| `insights[].title` | string | Yes | Short, actionable title | +| `insights[].description` | string | Yes | Detailed explanation | +| `insights[].category` | string | Yes | "strategy", "operations", "finance", "marketing", "leadership", "technology" | +| `insights[].priority` | string | Yes | "low", "medium", "high", "critical" | +| `insights[].kissCategory` | string | Yes | "keep", "improve", "start", "stop" | +| `insights[].alignmentImpact` | string | No | How this relates to business purpose/values | +| `insights[].status` | string | No | Defaults to "active" | +| `insights[].businessImpact` | string | No | "low", "medium", "high" | +| `insights[].effortRequired` | string | No | "low", "medium", "high" | + +### Response + +```json +{ + "success": true, + "data": { + "savedCount": 1, + "insights": [ + { + "id": "5caa3a0c-2751-4326-b5db-f13b945fdc53", + "tenantId": "dfee9135-361e-41b5-87ba-99e92f143666", + "title": "Focus on Enterprise Segment", + "description": "Analysis shows 40% higher conversion rates...", + "category": "strategy", + "priority": "high", + "kissCategory": "start", + "alignmentImpact": "This insight directly supports our growth objectives.", + "status": "active", + "businessImpact": "high", + "effortRequired": "medium", + "createdAt": "2026-02-04T02:51:52.836Z", + "updatedAt": "2026-02-04T02:51:52.836Z" + } + ] + }, + "error": null +} +``` + +**Notes:** +- **Input/Output**: Uses standard camelCase for all properties (consistent with all other API endpoints) +- Backend auto-generates GUID `id` if not provided +- Backend auto-generates timestamps if not provided + +--- + +## Get All Coaching Insights + +**GET** `/traction/api/v1/insights` + +Retrieve all insights for authenticated tenant. + +### Response + +```json +{ + "success": true, + "data": [ + { + "id": "5caa3a0c-2751-4326-b5db-f13b945fdc53", + "tenantId": "dfee9135-361e-41b5-87ba-99e92f143666", + "title": "Focus on Enterprise Segment", + "description": "Analysis shows 40% higher conversion rates...", + "category": "strategy", + "priority": "high", + "kissCategory": "start", + "alignmentImpact": "This insight directly supports our growth objectives.", + "status": "active", + "businessImpact": "high", + "effortRequired": "medium", + "createdAt": "2026-02-04T02:51:52.836Z", + "updatedAt": "2026-02-04T02:51:52.836Z" + } + ], + "error": null +} +``` + +--- + +## Get Coaching Insight by ID + +**GET** `/traction/api/v1/insights/{id}` + +Retrieve specific insight by ID. + +### Response + +```json +{ + "success": true, + "data": { + "id": "5caa3a0c-2751-4326-b5db-f13b945fdc53", + "tenantId": "dfee9135-361e-41b5-87ba-99e92f143666", + "title": "Focus on Enterprise Segment", + "description": "Analysis shows 40% higher conversion rates...", + "category": "strategy", + "priority": "high", + "kissCategory": "start", + "alignmentImpact": "This insight directly supports our growth objectives.", + "status": "active", + "businessImpact": "high", + "effortRequired": "medium", + "createdAt": "2026-02-04T02:51:52.836Z", + "updatedAt": "2026-02-04T02:51:52.836Z" + }, + "error": null +} +``` + +--- + +## Update Coaching Insight + +**PUT** `/traction/api/v1/insights/{id}` + +Update existing insight (partial updates supported). + +### Request Body + +```json +{ + "title": "Updated Title", + "description": "Updated description", + "priority": "critical", + "status": "in_progress" +} +``` + +### Response + +```json +{ + "success": true, + "data": { + "id": "5caa3a0c-2751-4326-b5db-f13b945fdc53", + "tenantId": "dfee9135-361e-41b5-87ba-99e92f143666", + "title": "Updated Title", + "description": "Updated description", + "category": "strategy", + "priority": "critical", + "kissCategory": "start", + "alignmentImpact": "This insight directly supports our growth objectives.", + "status": "in_progress", + "businessImpact": "high", + "effortRequired": "medium", + "createdAt": "2026-02-04T02:51:52.836Z", + "updatedAt": "2026-02-04T10:15:22.123Z" + }, + "error": null +} +``` + +**Notes:** +- Only provided fields updated +- `kissCategory`, `category`, `alignmentImpact`, `businessImpact`, `effortRequired` not updatable (set at creation) + +--- + +## Delete Coaching Insight + +**DELETE** `/traction/api/v1/insights/{id}` + +Permanently delete insight. + +### Response + +```json +{ + "success": true, + "data": null, + "error": null +} +``` + +--- + +## Frontend Integration + +**Service:** `src/services/api.ts` + +```typescript +// Save insights from Python AI (snake_case input) +await saveCoachingInsights({ insights: pythonAiResponse }); + +// Retrieve insights (camelCase output) +const insights = await getCoachingInsights(); + +// Update insight status +await updateCoachingInsight(id, { status: 'completed' }); +``` + +**Related:** +- [Python AI Service](../../ai-user/backend-integration-unified-ai.md) - Generates insights +- [Dashboard Widget](../dashboard-service.md#ai-insights-widget) - Displays insights diff --git a/docs/shared/Specifications/user-app/traction-service/issues-api.md b/docs/shared/Specifications/user-app/traction-service/issues-api.md new file mode 100644 index 00000000..878216c3 --- /dev/null +++ b/docs/shared/Specifications/user-app/traction-service/issues-api.md @@ -0,0 +1,1811 @@ +# Issues API Specification + +**Service:** Traction Service +**Base Path:** `/api/issues` +**Version:** v7.1 +**Last Updated:** January 4, 2026 + +## Overview + +The Issues API manages operational issues, bugs, blockers, and problems within the PurposePath system. Unlike Goals or Actions, Issues represent problems that need resolution rather than outcomes or tasks. Issues support configurable types and statuses, impact/priority tracking, lifecycle management, and can be converted into actionable items. + +### Key Features + +- **Configurable Status & Types**: Uses `IssueStatusConfigId` and `IssueTypeConfigId` for tenant-customizable workflows +- **Lifecycle Management**: Start, resolve, close, and reopen issues with specific endpoints +- **Impact & Priority Tracking**: Separate dimensions for business impact vs urgency +- **Tag-Based Organization**: Add/remove tags for flexible categorization +- **Relationship Management**: Link to goals, strategies, and actions +- **Issue Conversion**: Convert issues into one or more actions +- **Statistics Dashboard**: Aggregated metrics by status, type, impact, and timeline + +### Design Notes + +- **Configuration-Based Status/Types**: Unlike other APIs using simple enums, Issues use configurable status and type systems +- **Lifecycle Endpoints**: Dedicated endpoints for state transitions (start, resolve, close, reopen) ensure proper workflow +- **Bug-Specific Fields**: Includes environment, steps to reproduce, expected/actual behavior for bug tracking +- **Comma-Separated OR Filtering**: Priority and impact support OR logic (e.g., "high,critical") + +--- + +## Endpoints Summary + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/issues` | List issues with filtering, sorting, pagination | +| POST | `/issues` | Create new issue with optional connections | +| GET | `/issues/{issueId}` | Get single issue by ID | +| PUT | `/issues/{issueId}` | Update issue fields | +| DELETE | `/issues/{issueId}` | Delete issue (soft delete) | +| PUT | `/issues/{issueId}/status` | Update issue status via config ID | +| PUT | `/issues/{issueId}/assign` | Assign issue to user | +| PUT | `/issues/{issueId}/start` | Start work on issue | +| PUT | `/issues/{issueId}/resolve` | Resolve issue with notes | +| PUT | `/issues/{issueId}/close` | Close issue | +| PUT | `/issues/{issueId}/reopen` | Reopen closed/resolved issue | +| PUT | `/issues/{issueId}/tags` | Add tags to issue | +| DELETE | `/issues/{issueId}/tags` | Remove tags from issue | +| GET | `/issues/stats` | Get aggregated issue statistics | +| POST | `/issues/{issueId}/convert-to-actions` | Convert issue to actions | +| PUT | `/issues:reorder` | Reorder issues (drag-and-drop sorting) | + +--- + +## Common Data Models + +### IssueResponse + +```typescript +interface IssueResponse { + id: string; // "issue_123abc" format + title: string; // Max 200 characters + description: string; // Max 2000 characters + typeConfigId: string; // UUID of IssueTypeConfig + statusConfigId: string; // UUID of IssueStatusConfig + impact: 'low' | 'medium' | 'high' | 'critical'; + priority: 'low' | 'medium' | 'high' | 'critical'; + reporterId: string; // User ID who reported + reporterName?: string; // Display name (optional) + assignedPersonId?: string; // User ID assigned to + assignedPersonName?: string; // Display name (optional) + dueDate?: string; // ISO 8601 date (nullable) + estimatedHours?: number; // 0-1000 range + actualHours?: number; // 0-1000 range + tags: string[]; // Array of tag strings + + // Bug-specific fields + environment?: string; // Environment where issue occurs + stepsToReproduce?: string; // How to reproduce + expectedBehavior?: string; // What should happen + actualBehavior?: string; // What actually happens + + // Resolution fields + rootCauseAnalysis?: string; // Analysis of root cause + resolutionNotes?: string; // How issue was resolved + + displayOrder: number; // Custom ordering (>= 0) + + connections: { + goalIds: string[]; + strategyIds: string[]; + actionIds: string[]; + }; + + createdAt: string; // ISO 8601 timestamp + updatedAt: string; // ISO 8601 timestamp + createdBy: string; // User ID + updatedBy: string; // User ID +} +``` + +### Enumerations + +```typescript +// IssueImpact - Business impact level +enum IssueImpact { + Low = 'low', + Medium = 'medium', + High = 'high', + Critical = 'critical' +} + +// Priority - Urgency level (same as Actions) +enum Priority { + Low = 'low', + Medium = 'medium', + High = 'high', + Critical = 'critical' +} +``` + +**Impact vs Priority**: +- **Impact**: Business consequence if unresolved (data loss = high, typo = low) +- **Priority**: Time urgency (blocking production = critical, nice-to-have = low) + +--- + +## Endpoint Details + +### 1. List Issues + +**GET** `/issues` + +Retrieve paginated list of issues with comprehensive filtering options. + +#### Query Parameters + +```typescript +interface GetIssuesParams { + // Pagination + page?: number; // Default: 1 + limit?: number; // Default: 20, max: 100 + + // Filtering + status?: string; // Comma-separated statusConfigIds for OR logic + statusConfigId?: string; // Single status config ID + statusCategory?: string; // "open" | "active" | "inactive" | "closed" + typeConfigId?: string; // Issue type config ID + businessImpact?: string; // Comma-separated: "low,medium,high,critical" (OR logic) + priority?: string; // Comma-separated: "low,medium,high,critical" (OR logic) + assignedPersonId?: string; // Filter by assignee + reporterId?: string; // Filter by reporter + + // Date range filtering + createdAfter?: string; // ISO 8601 date + createdBefore?: string; // ISO 8601 date + dueAfter?: string; // ISO 8601 date + dueBefore?: string; // ISO 8601 date + + // Tag filtering + tags?: string; // Comma-separated: "bug,urgent" + + // Relationship filtering + actionIds?: string; // Comma-separated action IDs + goalIds?: string; // Comma-separated goal IDs + strategyIds?: string; // Comma-separated strategy IDs + + // Sorting + sort?: 'title' | 'status' | 'type' | 'impact' | 'priority' | 'dueDate' | 'createdAt' | 'updatedAt'; + order?: 'asc' | 'desc'; // Default: 'desc' +} +``` + +#### Response + +**Success (200 OK)** +```json +{ + "success": true, + "data": [ + { + "id": "issue_abc123", + "title": "Database connection timeout in production", + "description": "Users experiencing 5-second delays on dashboard load", + "typeConfigId": "550e8400-e29b-41d4-a716-446655440001", + "statusConfigId": "550e8400-e29b-41d4-a716-446655440010", + "impact": "high", + "priority": "critical", + "reporterId": "user_xyz789", + "reporterName": "John Smith", + "assignedPersonId": "user_abc456", + "assignedPersonName": "Jane Doe", + "dueDate": "2025-01-15", + "estimatedHours": 8.0, + "actualHours": null, + "tags": ["bug", "performance", "database"], + "environment": "Production AWS us-east-1", + "stepsToReproduce": "1. Login as admin\n2. Navigate to dashboard\n3. Observe 5s delay", + "expectedBehavior": "Dashboard loads within 1 second", + "actualBehavior": "Dashboard takes 5+ seconds to load", + "rootCauseAnalysis": null, + "resolutionNotes": null, + "displayOrder": 1, + "connections": { + "goalIds": ["goal_123"], + "strategyIds": ["strat_456"], + "actionIds": [] + }, + "createdAt": "2025-01-10T14:30:00Z", + "updatedAt": "2025-01-10T15:45:00Z", + "createdBy": "user_xyz789", + "updatedBy": "user_abc456" + } + ], + "pagination": { + "page": 1, + "limit": 20, + "totalPages": 3, + "totalItems": 52, + "hasNextPage": true, + "hasPreviousPage": false + } +} +``` + +#### Business Rules + +- **Comma-Separated OR Logic**: `businessImpact=high,critical` returns issues with high OR critical impact +- **Filter Combination**: Multiple filters use AND logic (e.g., priority AND impact AND assignedPersonId) +- **Default Sorting**: By `createdAt desc` (newest first) +- **Pagination Limits**: Max 100 items per page + +#### Validation Rules + +- `page`: Must be >= 1 +- `limit`: Must be 1-100 +- `sort`: Must be one of allowed values +- `order`: Must be "asc" or "desc" +- Date filters: Must be valid ISO 8601 dates + +#### Error Responses + +**400 Bad Request** +```json +{ + "success": false, + "error": "Invalid sort field. Allowed: title, status, type, impact, priority, dueDate, createdAt, updatedAt" +} +``` + +**401 Unauthorized** +```json +{ + "success": false, + "error": "Missing or invalid authentication token" +} +``` + +--- + +### 2. Create Issue + +**POST** `/issues` + +Create a new issue with optional connections to goals, strategies, and actions. + +#### Request Body + +```typescript +interface CreateIssueRequest { + title: string; // Required, max 200 chars + description?: string; // Optional, max 2000 chars + typeConfigId?: string; // Optional (defaults to Personnel type) + businessImpact: 'low' | 'medium' | 'high' | 'critical'; // Required + priority: 'low' | 'medium' | 'high' | 'critical'; // Required + reportedBy: string; // Required, user ID + dateReported?: string; // Optional, ISO 8601 (defaults to now) + statusId?: string; // Optional (defaults to "Identified") + assignedPersonId?: string; // Optional + dueDate?: string; // Optional, ISO 8601 date + estimatedHours?: number; // Optional, 0-1000 range + tags?: string[]; // Optional + environment?: string; // Optional, for bugs + stepsToReproduce?: string; // Optional, for bugs + expectedBehavior?: string; // Optional, for bugs + actualBehavior?: string; // Optional, for bugs + displayOrder?: number; // Optional, >= 0 + connections?: { + goalIds?: string[]; + strategyIds?: string[]; + actionIds?: string[]; + }; +} +``` + +#### Example Request + +```json +{ + "title": "API timeout on /dashboard endpoint", + "description": "The dashboard endpoint times out after 30 seconds under high load", + "typeConfigId": "550e8400-e29b-41d4-a716-446655440001", + "businessImpact": "high", + "priority": "critical", + "reportedBy": "user_reporter123", + "dateReported": "2025-01-10T10:00:00Z", + "assignedPersonId": "user_dev456", + "dueDate": "2025-01-20", + "estimatedHours": 12.0, + "tags": ["bug", "api", "performance"], + "environment": "Production - AWS us-east-1", + "stepsToReproduce": "1. Send 100 concurrent requests to /dashboard\n2. Observe timeout errors", + "expectedBehavior": "API responds within 5 seconds", + "actualBehavior": "API times out after 30 seconds", + "displayOrder": 1, + "connections": { + "goalIds": ["goal_perf2025"], + "strategyIds": ["strat_api_optimization"] + } +} +``` + +#### Response + +**Success (201 Created)** +```json +{ + "success": true, + "data": { + "id": "issue_new123", + "title": "API timeout on /dashboard endpoint", + "description": "The dashboard endpoint times out after 30 seconds under high load", + "typeConfigId": "550e8400-e29b-41d4-a716-446655440001", + "statusConfigId": "550e8400-e29b-41d4-a716-446655440010", + "impact": "high", + "priority": "critical", + "reporterId": "user_reporter123", + "assignedPersonId": "user_dev456", + "dueDate": "2025-01-20", + "estimatedHours": 12.0, + "actualHours": null, + "tags": ["bug", "api", "performance"], + "environment": "Production - AWS us-east-1", + "stepsToReproduce": "1. Send 100 concurrent requests to /dashboard\n2. Observe timeout errors", + "expectedBehavior": "API responds within 5 seconds", + "actualBehavior": "API times out after 30 seconds", + "displayOrder": 1, + "connections": { + "goalIds": ["goal_perf2025"], + "strategyIds": ["strat_api_optimization"], + "actionIds": [] + }, + "createdAt": "2025-01-10T10:00:00Z", + "updatedAt": "2025-01-10T10:00:00Z", + "createdBy": "user_reporter123", + "updatedBy": "user_reporter123" + } +} +``` + +#### Business Rules + +- **Default Type**: If `typeConfigId` not provided, uses default Personnel type +- **Default Status**: If `statusId` not provided, uses default "Identified" status +- **Default Date Reported**: If `dateReported` not provided, uses current timestamp +- **Automatic Connections**: Creates links to specified goals, strategies, actions atomically + +#### Validation Rules + +- `title`: Required, 1-200 characters +- `description`: Max 2000 characters +- `businessImpact`: Required, must be low/medium/high/critical +- `priority`: Required, must be low/medium/high/critical +- `reportedBy`: Required, must be valid user ID +- `estimatedHours`: 0-1000 range +- `displayOrder`: Must be >= 0 +- `dueDate`: Must be valid ISO 8601 date + +#### Error Responses + +**400 Bad Request** +```json +{ + "success": false, + "error": "Business impact must be one of: low, medium, high, critical" +} +``` + +**404 Not Found** +```json +{ + "success": false, + "error": "Type configuration not found" +} +``` + +--- + +### 3. Get Issue + +**GET** `/issues/{issueId}` + +Retrieve a single issue by its ID. + +#### Path Parameters + +- `issueId` (string, required): Issue identifier + +#### Response + +**Success (200 OK)** +```json +{ + "success": true, + "data": { + "id": "issue_abc123", + "title": "Database connection timeout in production", + "description": "Users experiencing 5-second delays on dashboard load", + "typeConfigId": "550e8400-e29b-41d4-a716-446655440001", + "statusConfigId": "550e8400-e29b-41d4-a716-446655440010", + "impact": "high", + "priority": "critical", + "reporterId": "user_xyz789", + "reporterName": "John Smith", + "assignedPersonId": "user_abc456", + "assignedPersonName": "Jane Doe", + "dueDate": "2025-01-15", + "estimatedHours": 8.0, + "actualHours": 6.5, + "tags": ["bug", "performance", "database"], + "environment": "Production AWS us-east-1", + "stepsToReproduce": "1. Login as admin\n2. Navigate to dashboard\n3. Observe 5s delay", + "expectedBehavior": "Dashboard loads within 1 second", + "actualBehavior": "Dashboard takes 5+ seconds to load", + "rootCauseAnalysis": "Connection pool exhaustion due to missing connection timeout", + "resolutionNotes": "Added 10s connection timeout and increased pool size from 10 to 20", + "displayOrder": 1, + "connections": { + "goalIds": ["goal_123"], + "strategyIds": ["strat_456"], + "actionIds": ["action_789"] + }, + "createdAt": "2025-01-10T14:30:00Z", + "updatedAt": "2025-01-12T16:20:00Z", + "createdBy": "user_xyz789", + "updatedBy": "user_abc456" + } +} +``` + +#### Error Responses + +**404 Not Found** +```json +{ + "success": false, + "error": "Issue not found" +} +``` + +--- + +### 4. Update Issue + +**PUT** `/issues/{issueId}` + +Update one or more fields of an existing issue. All fields are optional - only provided fields will be updated. + +#### Path Parameters + +- `issueId` (string, required): Issue identifier + +#### Request Body + +```typescript +interface UpdateIssueRequest { + title?: string; // Max 200 chars + description?: string; // Max 2000 chars + typeConfigId?: string; // Change issue type + businessImpact?: 'low' | 'medium' | 'high' | 'critical'; + priority?: 'low' | 'medium' | 'high' | 'critical'; + assignedPersonId?: string; + dueDate?: string; // ISO 8601 date + estimatedHours?: number; // 0-1000 range + actualHours?: number; // 0-1000 range + tags?: string[]; // Replaces existing tags + environment?: string; + stepsToReproduce?: string; + expectedBehavior?: string; + actualBehavior?: string; + rootCauseAnalysis?: string; + resolutionNotes?: string; + displayOrder?: number; // >= 0 + statusConfigId?: string; // Change status (prefer /status endpoint) +} +``` + +#### Example Request + +```json +{ + "priority": "high", + "assignedPersonId": "user_newdev789", + "estimatedHours": 10.0, + "tags": ["bug", "performance", "database", "urgent"] +} +``` + +#### Response + +**Success (200 OK)** +```json +{ + "success": true, + "data": { + "id": "issue_abc123", + "title": "Database connection timeout in production", + "priority": "high", + "assignedPersonId": "user_newdev789", + "assignedPersonName": "Alice Johnson", + "estimatedHours": 10.0, + "tags": ["bug", "performance", "database", "urgent"], + "updatedAt": "2025-01-12T17:00:00Z", + "updatedBy": "user_currentuser" + // ... other fields unchanged + } +} +``` + +#### Business Rules + +- **Partial Updates**: Only provided fields are updated +- **Tag Replacement**: Providing `tags` replaces entire tag array (not merge) +- **Validation**: Only validates fields that are provided +- **Status Changes**: Prefer dedicated `/status` endpoint for status changes + +#### Validation Rules + +- `title`: 1-200 characters (if provided) +- `description`: Max 2000 characters (if provided) +- `businessImpact`: Must be low/medium/high/critical (if provided) +- `priority`: Must be low/medium/high/critical (if provided) +- `estimatedHours`: 0-1000 range (if provided) +- `actualHours`: 0-1000 range (if provided) +- `displayOrder`: Must be >= 0 (if provided) + +#### Error Responses + +**400 Bad Request** +```json +{ + "success": false, + "error": "Priority must be one of: low, medium, high, critical" +} +``` + +**404 Not Found** +```json +{ + "success": false, + "error": "Issue not found" +} +``` + +--- + +### 5. Delete Issue + +**DELETE** `/issues/{issueId}` + +Delete an issue (soft delete). The issue is marked as deleted but not physically removed from the database. + +#### Path Parameters + +- `issueId` (string, required): Issue identifier + +#### Response + +**Success (200 OK)** +```json +{ + "success": true, + "data": { + "deletedIssueId": "issue_abc123", + "deletedAt": "2025-01-12T18:00:00Z" + } +} +``` + +#### Business Rules + +- **Soft Delete**: Issue is marked deleted, not physically removed +- **Cascade Behavior**: Connections to goals/strategies/actions are removed +- **Irreversible**: Cannot be undone via API (database restore only) + +#### Error Responses + +**404 Not Found** +```json +{ + "success": false, + "error": "Issue not found" +} +``` + +--- + +### 6. Update Issue Status + +**PUT** `/issues/{issueId}/status` + +Update issue status using a status configuration ID. This is the preferred method for status changes. + +#### Path Parameters + +- `issueId` (string, required): Issue identifier + +#### Request Body + +```typescript +interface UpdateIssueStatusRequest { + statusConfigId: string; // Required, UUID of status config + reason?: string; // Optional, reason for status change + resolutionNotes?: string; // Optional, notes about resolution + actualHours?: number; // Optional, 0-1000 range +} +``` + +#### Example Request + +```json +{ + "statusConfigId": "550e8400-e29b-41d4-a716-446655440020", + "reason": "Customer confirmed the fix works in production", + "resolutionNotes": "Increased connection pool size and added timeout configuration", + "actualHours": 8.5 +} +``` + +#### Response + +**Success (200 OK)** +```json +{ + "success": true, + "data": { + "id": "issue_abc123", + "statusConfigId": "550e8400-e29b-41d4-a716-446655440020", + "actualHours": 8.5, + "resolutionNotes": "Increased connection pool size and added timeout configuration", + "updatedAt": "2025-01-13T09:30:00Z" + // ... other fields + } +} +``` + +#### Business Rules + +- **Domain Event**: Publishes `IssueStatusChanged` event (handled by Issue #222 refactoring) +- **Status Validation**: Status config must exist and belong to tenant +- **Workflow Enforcement**: Some status transitions may be restricted (configured per tenant) + +#### Validation Rules + +- `statusConfigId`: Required, must be valid UUID +- `actualHours`: 0-1000 range (if provided) + +#### Error Responses + +**400 Bad Request** +```json +{ + "success": false, + "error": "Status configuration ID is required" +} +``` + +**404 Not Found** +```json +{ + "success": false, + "error": "Status configuration not found" +} +``` + +--- + +### 7. Assign Issue + +**PUT** `/issues/{issueId}/assign` + +Assign an issue to a specific user. + +#### Path Parameters + +- `issueId` (string, required): Issue identifier + +#### Request Body + +```typescript +interface AssignIssueRequest { + assignedPersonId: string; // Required, user ID + reason?: string; // Optional, reason for assignment +} +``` + +#### Example Request + +```json +{ + "assignedPersonId": "user_dev789", + "reason": "Alice has expertise in database performance optimization" +} +``` + +#### Response + +**Success (200 OK)** +```json +{ + "success": true, + "data": { + "id": "issue_abc123", + "assignedPersonId": "user_dev789", + "assignedPersonName": "Alice Chen", + "updatedAt": "2025-01-13T10:00:00Z" + // ... other fields + } +} +``` + +#### Business Rules + +- **Domain Event**: Publishes `IssueAssigned` event +- **Unassignment**: Pass empty string or null to unassign +- **User Validation**: Assigned user must exist in tenant + +#### Validation Rules + +- `assignedPersonId`: Required + +#### Error Responses + +**404 Not Found** +```json +{ + "success": false, + "error": "User not found or not in tenant" +} +``` + +--- + +### 8. Start Issue + +**PUT** `/issues/{issueId}/start` + +Mark an issue as started and transition to "In Progress" status. + +#### Path Parameters + +- `issueId` (string, required): Issue identifier + +#### Request Body + +```typescript +interface StartIssueRequest { + notes?: string; // Optional, notes about starting work +} +``` + +#### Example Request + +```json +{ + "notes": "Beginning investigation by reviewing application logs" +} +``` + +#### Response + +**Success (200 OK)** +```json +{ + "success": true, + "data": { + "id": "issue_abc123", + "statusConfigId": "550e8400-e29b-41d4-a716-446655440015", + "updatedAt": "2025-01-13T11:00:00Z" + // ... other fields + } +} +``` + +#### Business Rules + +- **Status Lookup**: Uses tenant's default "In Progress" status config +- **TODO Note**: Current implementation has placeholder for status config lookup +- **Domain Event**: Publishes `IssueStarted` event + +#### Error Responses + +**400 Bad Request** +```json +{ + "success": false, + "error": "InProgressStatusConfigId is required in request" +} +``` + +--- + +### 9. Resolve Issue + +**PUT** `/issues/{issueId}/resolve` + +Mark an issue as resolved with required resolution notes. + +#### Path Parameters + +- `issueId` (string, required): Issue identifier + +#### Request Body + +```typescript +interface ResolveIssueRequest { + resolutionNotes: string; // Required, how issue was resolved + rootCauseAnalysis?: string; // Optional, analysis of root cause + actualHours?: number; // Optional, 0-1000 range +} +``` + +#### Example Request + +```json +{ + "resolutionNotes": "Increased database connection pool from 10 to 20 connections and added 10-second timeout", + "rootCauseAnalysis": "Connection pool was exhausted during peak traffic due to missing timeout configuration, causing new requests to hang indefinitely", + "actualHours": 12.5 +} +``` + +#### Response + +**Success (200 OK)** +```json +{ + "success": true, + "data": { + "id": "issue_abc123", + "statusConfigId": "550e8400-e29b-41d4-a716-446655440025", + "resolutionNotes": "Increased database connection pool from 10 to 20 connections and added 10-second timeout", + "rootCauseAnalysis": "Connection pool was exhausted during peak traffic due to missing timeout configuration...", + "actualHours": 12.5, + "updatedAt": "2025-01-15T14:30:00Z" + // ... other fields + } +} +``` + +#### Business Rules + +- **Status Lookup**: Uses tenant's default "Resolved" status config +- **Required Notes**: Resolution notes are mandatory +- **Domain Event**: Publishes `IssueResolved` event +- **TODO Note**: Current implementation has placeholder for status config lookup + +#### Validation Rules + +- `resolutionNotes`: Required +- `actualHours`: 0-1000 range (if provided) + +#### Error Responses + +**400 Bad Request** +```json +{ + "success": false, + "error": "Resolution notes are required" +} +``` + +--- + +### 10. Close Issue + +**PUT** `/issues/{issueId}/close` + +Close an issue permanently. + +#### Path Parameters + +- `issueId` (string, required): Issue identifier + +#### Request Body + +```typescript +interface CloseIssueRequest { + reason?: string; // Optional, reason for closing +} +``` + +#### Example Request + +```json +{ + "reason": "Verified fix in production for 48 hours with no recurrence" +} +``` + +#### Response + +**Success (200 OK)** +```json +{ + "success": true, + "data": { + "id": "issue_abc123", + "statusConfigId": "550e8400-e29b-41d4-a716-446655440030", + "updatedAt": "2025-01-17T10:00:00Z" + // ... other fields + } +} +``` + +#### Business Rules + +- **Status Lookup**: Uses tenant's default "Closed" status config +- **Domain Event**: Publishes `IssueClosed` event +- **TODO Note**: Current implementation has placeholder for status config lookup + +--- + +### 11. Reopen Issue + +**PUT** `/issues/{issueId}/reopen` + +Reopen a closed or resolved issue. + +#### Path Parameters + +- `issueId` (string, required): Issue identifier + +#### Request Body + +```typescript +interface ReopenIssueRequest { + reason: string; // Required, reason for reopening +} +``` + +#### Example Request + +```json +{ + "reason": "Issue reoccurred in production - timeout errors returned after 72 hours" +} +``` + +#### Response + +**Success (200 OK)** +```json +{ + "success": true, + "data": { + "id": "issue_abc123", + "statusConfigId": "550e8400-e29b-41d4-a716-446655440012", + "updatedAt": "2025-01-20T09:00:00Z" + // ... other fields + } +} +``` + +#### Business Rules + +- **Status Lookup**: Uses tenant's default "Reopen" status config +- **Required Reason**: Must provide reason for reopening +- **Domain Event**: Publishes `IssueReopened` event +- **TODO Note**: Current implementation has placeholder for status config lookup + +#### Validation Rules + +- `reason`: Required + +#### Error Responses + +**400 Bad Request** +```json +{ + "success": false, + "error": "Reason is required for reopening an issue" +} +``` + +--- + +### 12. Add Tags to Issue + +**PUT** `/issues/{issueId}/tags` + +Add one or more tags to an issue. Uses batch command for optimized single database operation. + +#### Path Parameters + +- `issueId` (string, required): Issue identifier + +#### Request Body + +```typescript +interface AddIssueTagsRequest { + tags: string[]; // Required, array of tag strings +} +``` + +#### Example Request + +```json +{ + "tags": ["production", "hotfix", "customer-reported"] +} +``` + +#### Response + +**Success (200 OK)** +```json +{ + "success": true, + "data": { + "id": "issue_abc123", + "tags": ["bug", "performance", "database", "production", "hotfix", "customer-reported"], + "updatedAt": "2025-01-13T12:00:00Z" + // ... other fields + } +} +``` + +#### Business Rules + +- **Additive**: New tags are added to existing tags (not replaced) +- **Deduplication**: Duplicate tags are ignored +- **Batch Operation**: Single database write for all tags +- **Case-Sensitive**: Tags are case-sensitive + +#### Validation Rules + +- `tags`: Required, must be non-empty array + +#### Error Responses + +**400 Bad Request** +```json +{ + "success": false, + "error": "Tags array cannot be empty" +} +``` + +--- + +### 13. Remove Tags from Issue + +**DELETE** `/issues/{issueId}/tags` + +Remove one or more tags from an issue. Uses batch command for optimized single database operation. + +#### Path Parameters + +- `issueId` (string, required): Issue identifier + +#### Request Body + +```typescript +interface RemoveIssueTagsRequest { + tags: string[]; // Required, array of tag strings to remove +} +``` + +#### Example Request + +```json +{ + "tags": ["hotfix", "urgent"] +} +``` + +#### Response + +**Success (200 OK)** +```json +{ + "success": true, + "data": { + "id": "issue_abc123", + "tags": ["bug", "performance", "database", "production"], + "updatedAt": "2025-01-14T09:00:00Z" + // ... other fields + } +} +``` + +#### Business Rules + +- **Subtractive**: Specified tags are removed from existing tags +- **Non-Existent Tags**: Attempting to remove non-existent tags is ignored (no error) +- **Batch Operation**: Single database write for all tag removals +- **Case-Sensitive**: Tag matching is case-sensitive + +#### Validation Rules + +- `tags`: Required, must be non-empty array + +--- + +### 14. Get Issue Statistics + +**GET** `/issues/stats` + +Retrieve aggregated statistics about issues for the tenant. + +#### Response + +**Success (200 OK)** +```json +{ + "success": true, + "data": { + "totalIssues": 127, + "openIssues": 42, + "inProgressIssues": 18, + "resolvedIssues": 35, + "closedIssues": 30, + "overdueIssues": 7, + "criticalIssues": 5, + "resolutionRate": 0.73, + + "typeStats": { + "bugs": 45, + "features": 12, + "tasks": 38, + "improvements": 22, + "documentation": 10 + }, + + "impactStats": { + "low": 32, + "medium": 58, + "high": 28, + "critical": 9 + }, + + "timelineStats": { + "averageResolutionTimeHours": 16.5, + "issuesCreatedThisWeek": 8, + "issuesResolvedThisWeek": 12, + "issuesCreatedThisMonth": 34, + "issuesResolvedThisMonth": 38 + } + } +} +``` + +#### Data Model + +```typescript +interface IssueStatsData { + totalIssues: number; + openIssues: number; + inProgressIssues: number; + resolvedIssues: number; + closedIssues: number; + overdueIssues: number; + criticalIssues: number; + resolutionRate: number; // 0.0-1.0 (73% = 0.73) + + typeStats: { + bugs: number; + features: number; + tasks: number; + improvements: number; + documentation: number; + }; + + impactStats: { + low: number; + medium: number; + high: number; + critical: number; + }; + + timelineStats: { + averageResolutionTimeHours: number; + issuesCreatedThisWeek: number; + issuesResolvedThisWeek: number; + issuesCreatedThisMonth: number; + issuesResolvedThisMonth: number; + }; +} +``` + +#### Business Rules + +- **Tenant Scoped**: Statistics only include issues for current tenant +- **Resolution Rate**: (Resolved + Closed) / Total Issues +- **Overdue**: Issues with dueDate < now and status != closed/resolved + +--- + +### 15. Convert Issue to Actions + +**POST** `/issues/{issueId}/convert-to-actions` + +Convert an issue into one or more actions. Actions inherit the issue's goal/strategy connections and are automatically linked back to the issue. + +#### Path Parameters + +- `issueId` (string, required): Issue identifier + +#### Request Body + +```typescript +interface ConvertIssueToActionsRequest { + actions: Array<{ + title: string; // Required, max 200 chars + description?: string; // Optional, max 5000 chars + assignedPersonId?: string; // Optional + priority: 'low' | 'medium' | 'high' | 'critical'; // Required + }>; // Required, min 1 action + newStatusId?: string; // Optional, new status for issue after conversion +} +``` + +#### Example Request + +```json +{ + "actions": [ + { + "title": "Increase database connection pool size", + "description": "Increase connection pool from 10 to 20 in production configuration", + "assignedPersonId": "user_dev789", + "priority": "critical" + }, + { + "title": "Add connection timeout configuration", + "description": "Add 10-second timeout to prevent hanging connections", + "assignedPersonId": "user_dev789", + "priority": "critical" + }, + { + "title": "Monitor connection pool metrics", + "description": "Set up CloudWatch dashboards to monitor connection pool usage", + "assignedPersonId": "user_ops456", + "priority": "high" + } + ], + "newStatusId": "550e8400-e29b-41d4-a716-446655440015" +} +``` + +#### Response + +**Success (200 OK)** +```json +{ + "success": true, + "data": { + "actionIds": [ + "action_new001", + "action_new002", + "action_new003" + ] + } +} +``` + +#### Business Rules + +- **Inheritance**: Actions inherit issue's goal/strategy connections automatically +- **Bidirectional Link**: Actions are linked to the originating issue +- **Atomic Creation**: All actions created in single transaction (all or nothing) +- **Optional Status Change**: Can optionally update issue status after conversion +- **Action Properties**: Actions use issue's dueDate if no specific date provided + +#### Validation Rules + +- `actions`: Required, must have at least 1 action +- `actions[].title`: Required, max 200 characters +- `actions[].description`: Max 5000 characters +- `actions[].priority`: Required, must be low/medium/high/critical + +#### Error Responses + +**400 Bad Request** +```json +{ + "success": false, + "error": "At least one action is required" +} +``` + +**404 Not Found** +```json +{ + "success": false, + "error": "Issue not found" +} +``` + +--- + +### 16. Reorder Issues + +**PUT** `/issues:reorder` + +Reorder issues (drag-and-drop sorting). Updates the `displayOrder` field for each issue based on its position in the provided array. Similar to `/operations/issue-statuses:reorder` pattern. + +#### Request Body + +```typescript +interface ReorderIssuesRequest { + issueIds: string[]; // Ordered array of issue IDs (min 1 item) +} +``` + +#### Example Request + +```json +{ + "issueIds": [ + "issue_123abc", + "issue_456def", + "issue_789ghi", + "issue_012jkl" + ] +} +``` + +#### Response + +**Success (200 OK)** +```json +{ + "success": true, + "data": { + "issues": [ + { + "id": "issue_123abc", + "title": "Critical production bug", + "displayOrder": 0, + // ... other issue fields + }, + { + "id": "issue_456def", + "title": "Database connection timeout", + "displayOrder": 1, + // ... other issue fields + }, + { + "id": "issue_789ghi", + "title": "UI rendering issue", + "displayOrder": 2, + // ... other issue fields + }, + { + "id": "issue_012jkl", + "title": "Performance degradation", + "displayOrder": 3, + // ... other issue fields + } + ], + "total": 4, + "page": 1, + "limit": 4, + "totalPages": 1 + } +} +``` + +#### Business Rules + +- **Position-Based Ordering**: Array index determines new displayOrder (0-indexed) +- **Tenant Scoped**: All issues must belong to current tenant +- **Atomic Update**: All display orders updated in single transaction +- **No Gaps**: Display order is sequential based on array position +- **Validation**: Returns 400 if any issue ID is invalid or not found + +#### Validation Rules + +- `issueIds`: Required, must have at least 1 item +- All issue IDs must exist and belong to current tenant +- Issue IDs must be valid identifiers + +#### Error Responses + +**400 Bad Request** +```json +{ + "success": false, + "error": "At least one issue ID is required" +} +``` + +**400 Bad Request - Invalid Issue ID** +```json +{ + "success": false, + "error": "Issue with ID 'issue_invalid' not found" +} +``` + +**404 Not Found** +```json +{ + "success": false, + "error": "Issue with ID 'issue_123abc' not found" +} +``` + +--- + +## TypeScript Usage Examples + +### List Issues with Filtering + +```typescript +import axios from 'axios'; + +interface GetIssuesParams { + page?: number; + limit?: number; + priority?: string; + businessImpact?: string; + assignedPersonId?: string; + statusCategory?: string; + tags?: string; + sort?: string; + order?: string; +} + +async function listIssues(params: GetIssuesParams) { + const response = await axios.get('/api/issues', { + params, + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'X-Tenant-Id': tenantId + } + }); + + return response.data; +} + +// Get critical and high priority issues assigned to specific user +const criticalIssues = await listIssues({ + priority: 'critical,high', // OR logic + assignedPersonId: 'user_dev123', + statusCategory: 'open', + sort: 'dueDate', + order: 'asc' +}); + +// Get issues with specific tags +const taggedIssues = await listIssues({ + tags: 'bug,performance', + businessImpact: 'high,critical', // OR logic + sort: 'priority', + order: 'desc' +}); +``` + +### Create Issue with Connections + +```typescript +interface CreateIssuePayload { + title: string; + description?: string; + businessImpact: 'low' | 'medium' | 'high' | 'critical'; + priority: 'low' | 'medium' | 'high' | 'critical'; + reportedBy: string; + assignedPersonId?: string; + dueDate?: string; + estimatedHours?: number; + tags?: string[]; + environment?: string; + stepsToReproduce?: string; + expectedBehavior?: string; + actualBehavior?: string; + connections?: { + goalIds?: string[]; + strategyIds?: string[]; + }; +} + +async function createIssue(payload: CreateIssuePayload) { + const response = await axios.post('/api/issues', payload, { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'X-Tenant-Id': tenantId + } + }); + + return response.data; +} + +// Create bug with connections +const newIssue = await createIssue({ + title: 'Login page crashes on Safari', + description: 'Users on Safari 17+ experience crash when clicking login button', + businessImpact: 'high', + priority: 'critical', + reportedBy: 'user_support123', + assignedPersonId: 'user_frontend456', + dueDate: '2025-01-25', + estimatedHours: 8, + tags: ['bug', 'frontend', 'safari', 'login'], + environment: 'Production - Safari 17.2 on macOS Sonoma', + stepsToReproduce: '1. Open login page in Safari\n2. Enter credentials\n3. Click login button', + expectedBehavior: 'User is logged in and redirected to dashboard', + actualBehavior: 'Browser tab crashes immediately', + connections: { + goalIds: ['goal_stability_2025'], + strategyIds: ['strat_browser_compatibility'] + } +}); +``` + +### Issue Lifecycle Management + +```typescript +// Start working on issue +async function startIssue(issueId: string, notes?: string) { + const response = await axios.put( + `/api/issues/${issueId}/start`, + { notes }, + { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'X-Tenant-Id': tenantId + } + } + ); + + return response.data; +} + +// Resolve issue +async function resolveIssue( + issueId: string, + resolutionNotes: string, + rootCauseAnalysis?: string, + actualHours?: number +) { + const response = await axios.put( + `/api/issues/${issueId}/resolve`, + { + resolutionNotes, + rootCauseAnalysis, + actualHours + }, + { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'X-Tenant-Id': tenantId + } + } + ); + + return response.data; +} + +// Workflow example +const issue = await createIssue({ /* ... */ }); +await startIssue(issue.data.id, 'Beginning investigation'); +// ... work on issue ... +await resolveIssue( + issue.data.id, + 'Fixed by updating Safari-specific CSS transform', + 'Safari 17+ changed transform-origin behavior causing layout crash', + 6.5 +); +``` + +### Convert Issue to Actions + +```typescript +interface ActionToCreate { + title: string; + description?: string; + assignedPersonId?: string; + priority: 'low' | 'medium' | 'high' | 'critical'; +} + +async function convertIssueToActions( + issueId: string, + actions: ActionToCreate[], + newStatusId?: string +) { + const response = await axios.post( + `/api/issues/${issueId}/convert-to-actions`, + { + actions, + newStatusId + }, + { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'X-Tenant-Id': tenantId + } + } + ); + + return response.data; +} + +// Convert complex issue into multiple actions +const conversionResult = await convertIssueToActions( + 'issue_complex123', + [ + { + title: 'Update Safari CSS transforms', + description: 'Fix transform-origin issue in Safari 17+', + assignedPersonId: 'user_css_expert', + priority: 'critical' + }, + { + title: 'Add Safari 17+ compatibility tests', + description: 'Prevent regression in future Safari versions', + assignedPersonId: 'user_qa_lead', + priority: 'high' + }, + { + title: 'Update browser compatibility documentation', + description: 'Document Safari 17+ quirks', + assignedPersonId: 'user_tech_writer', + priority: 'medium' + } + ], + 'status_in_progress' // Update issue to "In Progress" after conversion +); + +console.log(`Created ${conversionResult.data.actionIds.length} actions from issue`); +``` + +### Tag Management + +```typescript +// Add tags +async function addTags(issueId: string, tags: string[]) { + const response = await axios.put( + `/api/issues/${issueId}/tags`, + { tags }, + { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'X-Tenant-Id': tenantId + } + } + ); + + return response.data; +} + +// Remove tags +async function removeTags(issueId: string, tags: string[]) { + const response = await axios.delete( + `/api/issues/${issueId}/tags`, + { + data: { tags }, + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'X-Tenant-Id': tenantId + } + } + ); + + return response.data; +} + +// Tag management workflow +await addTags('issue_abc123', ['urgent', 'customer-impact', 'production']); +await removeTags('issue_abc123', ['urgent']); // Issue addressed +``` + +### Get Issue Statistics + +```typescript +async function getIssueStats() { + const response = await axios.get('/api/issues/stats', { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'X-Tenant-Id': tenantId + } + }); + + return response.data; +} + +// Dashboard metrics +const stats = await getIssueStats(); +console.log(`Total Issues: ${stats.data.totalIssues}`); +console.log(`Critical Issues: ${stats.data.criticalIssues}`); +console.log(`Resolution Rate: ${(stats.data.resolutionRate * 100).toFixed(1)}%`); +console.log(`Average Resolution Time: ${stats.data.timelineStats.averageResolutionTimeHours} hours`); +``` + +--- + +## Migration Notes from v6 + +### Configuration-Based Status & Types + +**v6 (Simple Enums)** +```typescript +// Old approach used hardcoded status/type enums +status: 'open' | 'in_progress' | 'resolved' | 'closed' +type: 'bug' | 'feature' | 'task' +``` + +**v7 (Configuration-Based)** +```typescript +// New approach uses configurable status and type systems +statusConfigId: string; // UUID referencing IssueStatusConfig +typeConfigId: string; // UUID referencing IssueTypeConfig + +// Each tenant can define custom statuses and types +// Status configs include: id, name, category, displayOrder, isDefault +// Type configs include: id, name, description, icon, color +``` + +**Migration Impact**: Frontend must fetch status/type configurations separately and use IDs in requests instead of hardcoded enum values. + +### Lifecycle Endpoints + +**v6**: Single `/status` endpoint for all status changes + +**v7**: Dedicated endpoints for specific transitions +- `PUT /issues/{id}/start` - Start work +- `PUT /issues/{id}/resolve` - Mark resolved +- `PUT /issues/{id}/close` - Close issue +- `PUT /issues/{id}/reopen` - Reopen issue +- `PUT /issues/{id}/status` - Generic status change + +**Migration Strategy**: Use specific lifecycle endpoints for better semantics and validation. Generic `/status` endpoint still available for custom workflows. + +### Tag Management Optimization + +**v6**: Single-tag add/remove operations + +**v7**: Batch tag operations +- `PUT /issues/{id}/tags` with array - Add multiple tags atomically +- `DELETE /issues/{id}/tags` with array - Remove multiple tags atomically + +**Migration Strategy**: Update frontend to send tag arrays instead of individual tags for better performance. + +### Issue Conversion Feature + +**New in v7**: `POST /issues/{issueId}/convert-to-actions` + +Enables breaking down complex issues into actionable work items with automatic connection inheritance. Not available in v6. + +--- + +## Related APIs + +- **[Actions API](./actions-api.md)**: Created from issue conversion +- **[Goals API](./goals-api.md)**: Issue connections to goals +- **People API** (coming): Reporter and assignee details +- **Configuration API** (coming): IssueStatusConfig and IssueTypeConfig management + +--- + +## Changelog + +### v7.1 (January 4, 2026) +- **NEW**: Reorder issues endpoint (`PUT /issues:reorder`) for drag-and-drop sorting support + - Updates `displayOrder` field based on array position + - Supports atomic batch updates in single transaction + - Similar pattern to issue-statuses:reorder endpoint + +### v7.0 (December 23, 2025) +- **NEW**: Configuration-based status and types replacing hardcoded enums +- **NEW**: Dedicated lifecycle endpoints (start, resolve, close, reopen) +- **NEW**: Batch tag management (add/remove multiple tags atomically) +- **NEW**: Issue conversion to actions with inheritance +- **NEW**: Comprehensive filtering with comma-separated OR logic for priority/impact +- **NEW**: Issue statistics endpoint with type, impact, and timeline breakdowns +- **IMPROVED**: Bug-specific fields (environment, steps to reproduce, expected/actual behavior) +- **IMPROVED**: Root cause analysis and resolution notes tracking +- **CHANGED**: Status and type now use config IDs instead of enum strings + +--- + +## TODO / Known Limitations + +1. **Status Config Lookup**: Current implementation has placeholder for tenant default status config lookups (start, resolve, close, reopen endpoints) +2. **Relationship Endpoints**: Not yet implemented (coming soon): + - `PUT /api/issues/{issueId}/goals` + - `PUT /api/issues/{issueId}/strategies` + - `PUT /api/issues/{issueId}/actions` + - `DELETE /api/issues/{issueId}/relationships` +3. **Status Configuration Management**: Separate API needed for managing IssueStatusConfig and IssueTypeConfig entities +4. **Workflow Validation**: Status transition rules not yet enforced (e.g., can't resolve without being in-progress) + +--- + +## Support + +For questions or issues: +- **GitHub Issues**: [PurposePath Repository](https://github.com/purposepath/backend) +- **Slack**: #traction-service channel +- **Email**: backend-support@purposepath.com diff --git a/docs/shared/Specifications/user-app/traction-service/measure-data-api.md b/docs/shared/Specifications/user-app/traction-service/measure-data-api.md new file mode 100644 index 00000000..0d1c5ef2 --- /dev/null +++ b/docs/shared/Specifications/user-app/traction-service/measure-data-api.md @@ -0,0 +1,979 @@ +# Measure Data API Specification + +**Version:** 9.0 +**Last Updated:** January 8, 2026 +**Base Path:** `/measures/{measureId}` +**Controller:** `MeasureDataController.cs` + +## ⚠️ BREAKING CHANGES (v9.0 - Issue #557) + +**Released:** January 8, 2026 +**GitHub Issue:** [#557 - Fix MeasureData Architecture](https://github.com/purposepath/purposepath-api/issues/557) + +### Overview +Version 9.0 introduces a significant architectural improvement where Measure Data is now tied directly to Measures instead of MeasureLinks. This fixes the data model inconsistency where the same measure with multiple links had separate data sets. + +### URL Changes (REQUIRED UPDATE) +All endpoints have changed from `/measures/{measureId}/*` to `/measures/{measureId}/*`: + +| Old Endpoint (v8) | New Endpoint (v9) | +|-------------------|-------------------| +| `GET /measures/{measureId}/targets` | `GET /measures/{measureId}/targets` | +| `POST /measures/{measureId}/targets` | `POST /measures/{measureId}/targets` | +| `PUT /measures/{measureId}/targets/{targetId}` | `PUT /measures/{measureId}/targets/{targetId}` | +| `DELETE /measures/{measureId}/targets/{targetId}` | `DELETE /measures/{measureId}/targets/{targetId}` | +| `GET /measures/{measureId}/actuals` | `GET /measures/{measureId}/actuals` | +| `POST /measures/{measureId}/actuals` | `POST /measures/{measureId}/actuals` | +| `PUT /measures/{measureId}/actuals/{actualId}/override` | `PUT /measures/{measureId}/actuals/{actualId}/override` | +| `DELETE /measures/{measureId}/actuals/{actualId}` | `DELETE /measures/{measureId}/actuals/{actualId}` | +| `GET /measures/{measureId}/all-series` | `GET /measures/{measureId}/all-series` | + +### Response Field Changes (REQUIRED UPDATE) +All response DTOs now use `measureId` instead of `measureLinkId`: + +```json +// ❌ Old (v8) +{ + "measureId": "measure-456", + "targets": [...] +} + +// ✅ New (v9) +{ + "measureId": "measure-456", + "targets": [...] +} +``` + +### Migration Guide for Frontend Developers + +#### Step 1: Update API Service Base URLs +```typescript +// ❌ Old +const url = `/measure-links/${linkId}/targets`; + +// ✅ New - Get measureId from link object +const url = `/measures/${link.measureId}/targets`; +``` + +#### Step 2: Update Response Type Definitions +```typescript +// ❌ Old interface +interface MeasureDataResponse { + measureLinkId: string; + targets: TargetValue[]; + actuals: ActualValue[]; +} + +// ✅ New interface +interface MeasureDataResponse { + measureId: string; // Changed from measureLinkId + targets: TargetValue[]; + actuals: ActualValue[]; +} +``` + +#### Step 3: Update Component Props +```typescript +// ❌ Old +function MeasureChart({ measureLinkId }: Props) { + const data = await fetchMeasureData(measureLinkId); +} + +// ✅ New +function MeasureChart({ measureId }: Props) { + const data = await fetchMeasureData(measureId); +} +``` + +#### Step 4: Accessing measureId from Link Objects +```typescript +// All MeasureLink objects now have a measureId property +const link = await getMeasureLink(linkId); +const data = await fetchMeasureData(link.measureId); // Use this +``` + +### Data Persistence Changes +- DynamoDB partition keys changed from `MEASURELINK#{linkId}` to `MEASURE#{measureId}` +- GSI names changed: `measure-link-index` → `measure-index` +- All measure data is now stored once per Measure (not per Link) +- Multiple links to the same Measure share the same data set + +### Benefits of This Change +✅ **Architectural correctness:** Data properly tied to the Measure entity +✅ **Data consistency:** One Measure = one data set, regardless of links +✅ **Simplified queries:** No need to aggregate across multiple links +✅ **Widget performance:** Widgets query by measureId directly + +### Backward Compatibility +⚠️ **None** - This is a breaking change requiring frontend updates +⚠️ All old `/measures/{measureId}/*` endpoints will return 404 +⚠️ All `measureLinkId` fields in responses have been removed + +**Estimated Frontend Migration Time:** 1-2 days for a typical application + +--- + +## Overview + +The Measure Data API manages target values and actual measurements for Measures. This is the v9 architecture where data is tied directly to Measures (not MeasureLinks), consolidated with v8 data model that unifies Expected/Optimal/Minimal targets into a single record (issue #512, #557). + +### Key Features +- **Measure-scoped:** All data is associated with a Measure (not MeasureLink) +- **Consolidated Targets:** Set expected, optimal, and minimal values in a single record +- **Actuals:** Record measured values and estimates +- **Time-series data:** Track Measure progress over time +- **Override support:** Manual corrections with audit trail +- **Combined queries:** Get all targets + actuals in one request + +### Design Philosophy +- **Single record per target date:** Expected/Optimal/Minimal values stored together, not as separate records +- **Measure-scoped:** All data is associated with a Measure directly (architectural fix in v9) +- **Category-based:** `Target` (with optional optimal/minimal) or `Actual` (Measured/Estimate) +- **Historical tracking:** All values are preserved with timestamps +- **Simplified validation:** Three-value consistency enforced: `OptimalValue >= PostValue >= MinimalValue` + +--- + +## Authentication + +All endpoints require: +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +--- + +## Target Endpoints + +### 1. Get Targets + +Retrieve all target values for a Measure. + +**Endpoint:** `GET /measures/{measureId}/targets` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `measureId` | string (GUID) | **Yes** | Measure Measure identifier | + +#### Query Parameters + +None. All targets for the link are returned. + +#### Request Example + +```http +GET /measures/measure-456/targets +Authorization: Bearer {token} +X-Tenant-Id: {tenantId} +``` + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "measureId": "measure-456", + "targets": [ + { + "id": "target-001", + "measureId": "measure-456", + "dataCategory": "Target", + "actualSubtype": null, + "postValue": 50000.00, + "optimalValue": 60000.00, + "minimalValue": 45000.00, + "postDate": "2025-12-31T00:00:00Z", + "measuredPeriodStartDate": "2025-12-01T00:00:00Z", + "label": "Q4 2025 Target", + "confidenceLevel": 80, + "rationale": "Based on historical growth trends", + "originalValue": null, + "isManualOverride": false, + "overrideComment": null, + "dataSource": null, + "sourceReferenceId": null, + "createdAt": "2025-10-01T10:00:00Z", + "updatedAt": "2025-10-01T10:00:00Z" + } + ], + "totalCount": 1 + }, + "error": null +} +``` + +#### Field Descriptions + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string (GUID) | Unique Measure data entry identifier | +| `measureId` | string (GUID) | Associated Measure | +| `dataCategory` | enum | `Target` or `Actual` | +| `actualSubtype` | enum | For actuals: `Measured`, `Estimate`. Always null for targets. | +| `postValue` | decimal | The primary/expected target value | +| `optimalValue` | decimal (nullable) | **NEW:** Stretch goal value (optional) | +| `minimalValue` | decimal (nullable) | **NEW:** Minimum acceptable value (optional) | +| `postDate` | datetime | Target date or measurement date | +| `measuredPeriodStartDate` | datetime | Start of the measurement period | +| `label` | string | Human-readable label (e.g., "Q4 2025 Target") | +| `confidenceLevel` | int | Confidence percentage (0-100) | +| `rationale` | string | Explanation/justification for the value | +| `originalValue` | decimal | Original value before override (if overridden) | +| `isManualOverride` | boolean | Was this value manually overridden? | +| `overrideComment` | string | Comment explaining the override | +| `dataSource` | enum | Where data came from (e.g., `Manual`, `Automated`, `Integration`) | +| `sourceReferenceId` | string | External system reference | +| `createdAt` | datetime | When entry was created | +| `updatedAt` | datetime | Last update timestamp | + +#### Target Value Meanings + +- **postValue:** Standard/baseline target (primary target, always required) +- **optimalValue:** Stretch goal or best-case scenario (optional) +- **minimalValue:** Minimum acceptable performance threshold (optional) + +**Validation:** When both optimalValue and minimalValue are provided: `optimalValue >= postValue >= minimalValue` + +--- + +### 2. Create Target + +Set a new target value for a Measure link. Creates a single record with expected (primary) value and optional optimal/minimal values. + +**Endpoint:** `POST /measures/{measureId}/targets` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `measureId` | string (GUID) | **Yes** | Measure Measure identifier | + +#### Request Body + +```json +{ + "targetValue": 50000.00, + "optimalValue": 60000.00, + "minimalValue": 45000.00, + "targetDate": "2025-12-31T00:00:00Z", + "periodStartDate": "2025-12-01T00:00:00Z", + "label": "Q4 2025 Revenue Target", + "confidenceLevel": 80, + "rationale": "Based on current pipeline and historical conversion rates" +} +``` + +#### Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `targetValue` | decimal | **Yes** | Primary/expected target value | +| `optimalValue` | decimal | No | **NEW:** Stretch goal value (optional) | +| `minimalValue` | decimal | No | **NEW:** Minimum acceptable value (optional) | +| `targetDate` | datetime | **Yes** | When target should be achieved | +| `periodStartDate` | datetime | No | Start of measurement period | +| `label` | string | No | Human-readable label | +| `confidenceLevel` | int | No | Confidence percentage (0-100) | +| `rationale` | string | No | Explanation for this target | + +#### Response + +**Status:** `201 Created` + +```json +{ + "success": true, + "data": { + "id": "target-new-001", + "measureId": "measure-456", + "dataCategory": "Target", + "postValue": 50000.00, + "optimalValue": 60000.00, + "minimalValue": 45000.00, + "postDate": "2025-12-31T00:00:00Z", + "measuredPeriodStartDate": "2025-12-01T00:00:00Z", + "label": "Q4 2025 Revenue Target", + "confidenceLevel": 80, + "rationale": "Based on current pipeline and historical conversion rates", + "createdAt": "2025-12-23T16:30:00Z", + "updatedAt": "2025-12-23T16:30:00Z" + }, + "error": null +} +``` + +#### Business Rules + +- **Single record:** Creates one record with all three target values (if provided) +- **Required field:** `targetValue` (expected) is always required +- **Optional fields:** `optimalValue` and `minimalValue` are optional +- **Validation:** When both optimalValue and minimalValue are provided: `optimalValue >= targetValue >= minimalValue` +- **Date ranges:** `targetDate` is when the target should be achieved; `periodStartDate` is the measurement period start +- **Confidence level:** Higher values indicate more certainty about achieving the target + +#### Examples + +**Example 1: Expected target only** +```json +{ + "targetValue": 50000.00, + "targetDate": "2025-12-31T00:00:00Z" +} +``` + +**Example 2: All three target values** +```json +{ + "targetValue": 50000.00, + "optimalValue": 60000.00, + "minimalValue": 45000.00, + "targetDate": "2025-12-31T00:00:00Z", + "label": "Q4 2025 Revenue" +} +``` + +--- + +### 3. Update Target + +Update an existing target value or properties. + +**Endpoint:** `PUT /measures/{measureId}/targets/{targetId}` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `measureId` | string (GUID) | **Yes** | Measure Measure identifier | +| `targetId` | string (GUID) | **Yes** | Target entry identifier | + +#### Request Body + +```json +{ + "targetValue": 55000.00, + "optimalValue": 65000.00, + "minimalValue": 47000.00, + "label": "Q4 2025 Revenue Target (Revised)", + "confidenceLevel": 85, + "rationale": "Updated based on new deal closures" +} +``` + +#### Request Fields + +All fields are optional. Only provided fields will be updated. + +| Field | Type | Description | +|-------|------|-------------| +| `targetValue` | decimal | Updated expected/primary target value | +| `optimalValue` | decimal (nullable) | **NEW:** Updated stretch goal value | +| `minimalValue` | decimal (nullable) | **NEW:** Updated minimum acceptable value | +| `label` | string | Updated label | +| `confidenceLevel` | int | Updated confidence level (0-100) | +| `rationale` | string | Updated rationale | + +**Validation:** When both optimalValue and minimalValue are provided: `optimalValue >= targetValue >= minimalValue` + +**Note:** Cannot change `targetDate` via update. Delete and recreate instead. + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "id": "target-001", + "measureId": "measure-456", + "dataCategory": "Target", + "postValue": 55000.00, + "optimalValue": 65000.00, + "minimalValue": 47000.00, + "postDate": "2025-12-31T00:00:00Z", + "label": "Q4 2025 Revenue Target (Revised)", + "confidenceLevel": 85, + "rationale": "Updated based on new deal closures", + "updatedAt": "2025-12-23T17:00:00Z" + }, + "error": null +} +``` + +--- + +### 4. Delete Target + +Remove a target entry. + +**Endpoint:** `DELETE /measures/{measureId}/targets/{targetId}` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `measureId` | string (GUID) | **Yes** | Measure Measure identifier | +| `targetId` | string (GUID) | **Yes** | Target entry identifier | + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "message": "Target deleted successfully" + }, + "error": null +} +``` + +#### Business Rules + +- **Soft delete:** Target is marked as deleted but preserved for historical analysis +- **Cascade:** Does not affect actuals or the Measure link itself + +--- + +## Actual Endpoints + +### 5. Get Actuals + +Retrieve all actual measurements for a Measure link. + +**Endpoint:** `GET /measures/{measureId}/actuals` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `measureId` | string (GUID) | **Yes** | Measure Measure identifier | + +#### Query Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `actualSubtype` | enum | No | Filter by subtype: `Measured`, `Estimate` | + +#### Request Example + +```http +GET /measures/measure-456/actuals?actualSubtype=Measured +Authorization: Bearer {token} +X-Tenant-Id: {tenantId} +``` + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "measureId": "measure-456", + "actuals": [ + { + "id": "actual-001", + "measureId": "measure-456", + "dataCategory": "Actual", + "actualSubtype": "Measured", + "postValue": 48500.00, + "postDate": "2025-12-15T00:00:00Z", + "measuredPeriodStartDate": "2025-12-01T00:00:00Z", + "dataSource": "Automated", + "sourceReferenceId": "stripe-mrr-dec-2025", + "rationale": "Monthly recurring revenue from Stripe", + "originalValue": null, + "isManualOverride": false, + "createdAt": "2025-12-15T08:00:00Z", + "updatedAt": "2025-12-15T08:00:00Z" + } + ], + "totalCount": 1 + }, + "error": null +} +``` + +#### Actual Subtypes + +- **Measured:** Actual recorded value from a measurement or data source +- **Estimate:** Projected or estimated value (not yet measured) + +--- + +### 6. Record Actual + +Record a new actual measurement or estimate. + +**Endpoint:** `POST /measures/{measureId}/actuals` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `measureId` | string (GUID) | **Yes** | Measure Measure identifier | + +#### Request Body + +```json +{ + "actualSubtype": "Measured", + "actualValue": 48500.00, + "measurementDate": "2025-12-15T00:00:00Z", + "periodStartDate": "2025-12-01T00:00:00Z", + "dataSource": "Automated", + "sourceReferenceId": "stripe-mrr-dec-2025", + "rationale": "Automated sync from Stripe" +} +``` + +#### Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `actualSubtype` | enum | **Yes** | `Measured` or `Estimate` | +| `actualValue` | decimal | **Yes** | Actual measured/estimated value | +| `measurementDate` | datetime | **Yes** | When value was measured | +| `periodStartDate` | datetime | No | Start of measurement period | +| `dataSource` | string | No | Where data came from (e.g., "Manual", "Stripe", "Salesforce") | +| `sourceReferenceId` | string | No | External system reference ID | +| `rationale` | string | No | Explanation or context | + +#### Response + +**Status:** `201 Created` + +```json +{ + "success": true, + "data": { + "id": "actual-new-001", + "measureId": "measure-456", + "dataCategory": "Actual", + "actualSubtype": "Measured", + "postValue": 48500.00, + "postDate": "2025-12-15T00:00:00Z", + "measuredPeriodStartDate": "2025-12-01T00:00:00Z", + "dataSource": "Automated", + "sourceReferenceId": "stripe-mrr-dec-2025", + "rationale": "Automated sync from Stripe", + "createdAt": "2025-12-23T16:45:00Z", + "updatedAt": "2025-12-23T16:45:00Z" + }, + "error": null +} +``` + +#### Business Rules + +- **Historical series:** All actuals are preserved for time-series analysis +- **Measured vs Estimate:** Use `Measured` for confirmed values, `Estimate` for projections +- **Data source tracking:** `dataSource` and `sourceReferenceId` provide audit trail + +--- + +### 7. Override Actual + +Manually override an actual value with a correction. + +**Endpoint:** `PUT /measures/{measureId}/actuals/{actualId}/override` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `measureId` | string (GUID) | **Yes** | Measure Measure identifier | +| `actualId` | string (GUID) | **Yes** | Actual entry identifier | + +#### Request Body + +```json +{ + "newValue": 49000.00, + "overrideComment": "Corrected for refunds not reflected in automated sync" +} +``` + +#### Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `newValue` | decimal | **Yes** | Corrected value | +| `overrideComment` | string | **Yes** | Explanation for the override | + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "id": "actual-001", + "measureId": "measure-456", + "dataCategory": "Actual", + "actualSubtype": "Measured", + "postValue": 49000.00, + "originalValue": 48500.00, + "isManualOverride": true, + "overrideComment": "Corrected for refunds not reflected in automated sync", + "updatedAt": "2025-12-23T17:15:00Z" + }, + "error": null +} +``` + +#### Business Rules + +- **Audit trail:** Original value is preserved in `originalValue` field +- **Flag set:** `isManualOverride` is set to `true` +- **Required comment:** Override comment is mandatory for accountability + +--- + +### 8. Delete Actual + +Remove an actual measurement entry. + +**Endpoint:** `DELETE /measures/{measureId}/actuals/{actualId}` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `measureId` | string (GUID) | **Yes** | Measure identifier | +| `actualId` | string (GUID) | **Yes** | Actual entry identifier | + +#### Request Example + +```http +DELETE /measures/measure-456/actuals/actual-001 +Authorization: Bearer {token} +X-Tenant-Id: {tenantId} +``` + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "message": "Actual deleted successfully" + }, + "error": null +} +``` + +#### Business Rules + +- **Soft delete:** Actual is marked as deleted but preserved for historical analysis +- **Cascade:** Does not affect targets or the Measure itself +- **Audit trail:** Deletion is logged with user and timestamp + +--- + +## Combined Data Endpoint + +### 9. Get All Series + +Retrieve all targets and actuals for a Measure link in one request. + +**Endpoint:** `GET /measures/{measureId}/all-series` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `measureId` | string (GUID) | **Yes** | Measure Measure identifier | + +#### Request Example + +```http +GET /measures/measure-456/all-series +Authorization: Bearer {token} +X-Tenant-Id: {tenantId} +``` + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "measureId": "measure-456", + "targets": [ + { + "id": "target-001", + "postValue": 50000.00, + "optimalValue": 60000.00, + "minimalValue": 45000.00, + "postDate": "2025-12-31T00:00:00Z", + "label": "Q4 2025 Target" + } + ], + "actuals": [ + { + "id": "actual-001", + "actualSubtype": "Measured", + "postValue": 48500.00, + "postDate": "2025-12-15T00:00:00Z" + } + ], + "latestActual": { + "id": "actual-001", + "actualSubtype": "Measured", + "postValue": 48500.00, + "postDate": "2025-12-15T00:00:00Z" + }, + "totalTargets": 1, + "totalActuals": 1 + }, + "error": null +} +``` + +#### Use Cases + +- **Dashboard charts:** Single request for complete time-series visualization +- **Performance comparison:** Compare actuals against expected/optimal/minimal target levels in single record +- **Progress tracking:** Show current performance vs. targets + +--- + +## Data Models + +### Data Category Enum + +```typescript +enum DataCategory { + Target = "Target", // Target values + Actual = "Actual" // Actual measurements +} +``` + +### Actual Subtype Enum + +```typescript +enum ActualSubtype { + Measured = "Measured", // Actual measured value + Estimate = "Estimate" // Projected/estimated value +} +``` + +### Data Source Enum + +```typescript +enum DataSource { + Manual = "Manual", // Manually entered + Automated = "Automated", // Automated sync/import + Integration = "Integration" // External system integration +} +``` + +--- + +## Business Rules + +### Targets + +1. **Consolidated record:** Expected (primary), Optimal (stretch), and Minimal (floor) values stored in single record +2. **Required field:** `postValue` (expected) is always required for targets +3. **Optional fields:** `optimalValue` and `minimalValue` are optional +4. **Value validation:** When both optimalValue and minimalValue provided: `optimalValue >= postValue >= minimalValue` +5. **Time-series:** Can have multiple target entries for different future dates +6. **Confidence level:** Optional 0-100 value indicating certainty about achieving the target +7. **Rationale:** Recommended to document why a target was set at a specific value + +### Actuals + +1. **Historical preservation:** All actuals are kept for historical analysis +2. **Measured vs Estimate:** + - Use `Measured` for confirmed values from data sources + - Use `Estimate` for projections or manual estimates +3. **Override capability:** Can correct values with audit trail (original value + comment) +4. **Data source tracking:** Records where data came from and reference ID + +--- + +## Error Handling + +### Standard Error Response + +```json +{ + "success": false, + "data": null, + "error": "Error message here" +} +``` + +### Common Error Codes + +| Code | Scenario | Message Example | +|------|----------|-----------------| +| 400 | Invalid GUID format | "Invalid link ID format" | +| 400 | Missing required field | "TargetValue is required" | +| 400 | Invalid value order | "optimalValue must be >= targetValue and targetValue must be >= minimalValue" | +| 401 | Missing/invalid token | "Unauthorized" | +| 403 | Insufficient permissions | "Access denied to this Measure link" | +| 404 | Entry not found | "Target not found" | +| 422 | Validation failure | "MeasurementDate cannot be in the future" | +| 500 | Server error | "Internal server error" | + +--- + +## Frontend Usage Examples + +### TypeScript Service + +```typescript +import { traction } from './traction'; + +// Get all targets for a Measure link +const targets = await traction.get( + `/measure-links/${linkId}/targets` +); + +// Create a target with all three values (expected, optimal, minimal) +const newTarget = await traction.post( + `/measure-links/${linkId}/targets`, + { + targetValue: 50000.00, // Expected/primary (required) + optimalValue: 60000.00, // Stretch goal (optional) + minimalValue: 45000.00, // Floor (optional) + targetDate: '2025-12-31T00:00:00Z', + label: 'Q4 2025 Target', + confidenceLevel: 80, + rationale: 'Based on pipeline analysis' + } +); + +// Create a simple target (expected value only) +const simpleTarget = await traction.post( + `/measure-links/${linkId}/targets`, + { + targetValue: 50000.00, + targetDate: '2025-12-31T00:00:00Z' + } +); + +// Record a measured actual +const actual = await traction.post( + `/measure-links/${linkId}/actuals`, + { + actualSubtype: 'Measured', + actualValue: 48500.00, + measurementDate: '2025-12-15T00:00:00Z', + dataSource: 'Automated', + sourceReferenceId: 'stripe-mrr-dec-2025' + } +); + +// Override an actual with correction +await traction.put(`/measure-links/${linkId}/actuals/${actualId}/override`, { + newValue: 49000.00, + overrideComment: 'Corrected for data sync issue' +}); + +// Get all data (targets + actuals) for charts +const allData = await traction.get( + `/measure-links/${linkId}/all-series` +); + +// Chart the consolidated data +renderChart({ + targets: allData.data.targets, // Each target has postValue, optimalValue, minimalValue + actuals: allData.data.actuals, + latest: allData.data.latestActual +}); +``` + +--- + +## Migration from v6 + +### Deprecated Entities (Removed in v7) + +| Old Entity (v6) | New Entity (v7) | Notes | +|-----------------|-----------------|-------| +| `MeasureMilestone` | `MeasureData` (Target subtype) | Unified into single entity | +| `MeasureActual` | `MeasureData` (Actual subtype) | Unified into single entity | +| `MeasureReading` | `MeasureData` (Actual subtype) | Merged with MeasureActual | + +### Key Differences + +**v6 (Separate entities):** +- `MeasureMilestone` for target values +- `MeasureActual` for measured values +- `MeasureReading` for estimates +- Different endpoints for each type + +**v7 (Unified model):** +- Single `MeasureData` entity with `DataCategory` (Target/Actual) +- Target subtypes: Expected/Optimal/Minimal +- Actual subtypes: Measured/Estimate +- Consistent endpoints: `/measures/{measureId}/targets` and `/measures/{measureId}/actuals` + +### Benefits of v7 Design + +- ✅ Single entity reduces complexity +- ✅ Consistent API patterns +- ✅ Easier querying (all data in one table) +- ✅ Better time-series support +- ✅ Unified audit trail + +--- + +## Related APIs + +- **[Measure Links API](./measure-links-api.md)** - Link Measures to goals/persons/strategies +- **[Measures API](./measures-api.md)** - Manage Measures +- **[Goals API](./goals-api.md)** - Manage goals + +--- + +## Changelog + +### v8.0 (January 8, 2026) +- 🔄 **BREAKING:** Consolidated target values into single record (Issue #512) +- ✅ Expected/Optimal/Minimal values now stored together, not as separate records +- ✅ Added `optimalValue` and `minimalValue` optional fields to target records +- ❌ Removed `TargetSubtype` enum and `targetSubtype` field +- ❌ Removed `triggersReplan` and `replanThresholdExceeded` fields (unused feature) +- ❌ Removed Mark Replan Trigger endpoint (Issue #512) +- ✅ Single API call now creates all three target values atomically +- ✅ Added validation: `optimalValue >= postValue >= minimalValue` +- 📝 Updated all endpoints, examples, and data models +- 📝 Simplified business rules and response structures + +### v7.0 (December 23, 2025) +- ✨ New unified MeasureData model replacing MeasureMilestone/MeasureActual/MeasureReading (Issue #374) +- ✅ Documented 9 endpoints with complete examples +- ✨ Target subtypes: Expected, Optimal, Minimal +- ✨ Actual subtypes: Measured, Estimate +- ✨ Override capability with audit trail +- ✨ Replan trigger flagging +- ✨ Combined all-series endpoint for charts +- 📝 Complete request/response examples +- 📝 Business rules and validation documented +- 📝 Migration guide from v6 entities + +### v6.0 (December 21, 2025) +- ⚠️ Deprecated MeasureMilestone, MeasureActual, MeasureReading + +--- + +**[← Back to Traction Service Index](./README.md)** diff --git a/docs/shared/Specifications/user-app/traction-service/measure-links-api.md b/docs/shared/Specifications/user-app/traction-service/measure-links-api.md new file mode 100644 index 00000000..e4997b03 --- /dev/null +++ b/docs/shared/Specifications/user-app/traction-service/measure-links-api.md @@ -0,0 +1,805 @@ +# Measure Links API Specification + +**Version:** 8.4 +**Last Updated:** January 15, 2026 +**Base Path:** `/measure-links` +**Controller:** `MeasureLinksController.cs` + +## Overview + +The Measure Links API manages relationships between Measures and entities (persons, goals, strategies). Every Measure link requires a person (owner/responsible party) and can optionally be associated with a goal and/or strategy. + +### Key Features +- Link Measures to persons (required), with optional goal and strategy associations +- Configure thresholds, weights, and display order for each link +- Mark primary Measures for goals +- Query Measure links with flexible filtering (by person, goal, strategy, or Measure) +- Update or remove links +- Automatic cascade delete when tenant-specific Measures are deleted + +### Design Philosophy +- **Unified resource-based API:** All Measure link operations through `/measure-links` endpoints +- **Person-centric:** Every Measure link must have a person (owner) +- **Flexible filtering:** Query by any entity type (person, goal, strategy, Measure) with optional `includeAll` parameter +- **Derived linkType:** Link type is calculated based on foreign key presence (not persisted) +- **Multi-entity:** One Measure can be linked to multiple persons/goals/strategies simultaneously + +--- + +## Authentication + +All endpoints require: +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +--- + +## Endpoints + +### 1. Get Measure Link by ID + +Retrieve details of a specific Measure link. + +**Endpoint:** `GET /measure-links/{linkId}` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `linkId` | string (GUID) | **Yes** | Measure link identifier | + +#### Request Example + +```http +GET /measure-links/link-123e4567-e89b-12d3-a456-426614174000 +Authorization: Bearer {token} +X-Tenant-Id: {tenantId} +``` + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "id": "link-123e4567-e89b-12d3-a456-426614174000", + "measureId": "measure-456e7890-e89b-12d3-a456-426614174001", + "personId": "person-789e1234-e89b-12d3-a456-426614174002", + "goalId": "goal-012e3456-e89b-12d3-a456-426614174003", + "strategyId": "strategy-345e6789-e89b-12d3-a456-426614174004", + "thresholdPct": 80.0, + "riskThresholdPct": 50.0, + "linkType": "strategy", + "weight": 1.5, + "displayOrder": 1, + "isPrimary": true, + "linkedAt": "2025-12-20T10:00:00Z", + "progress": { + "progressPercentage": 85.5, + "status": "on_track", + "variance": 10.5, + "variancePercentage": 14.0, + "daysUntilTarget": 15, + "isOverdue": false + } + }, + "error": null +} +``` + +#### Field Descriptions + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string (GUID) | Unique link identifier | +| `measureId` | string (GUID) | Measure being linked | +| `personId` | string (GUID) | Person responsible (owner) - **Required** | +| `goalId` | string (GUID) | Goal linked to (nullable) | +| `strategyId` | string (GUID) | Strategy linked to (nullable) | +| `thresholdPct` | decimal | Completion threshold percentage (0-100) | +| `riskThresholdPct` | decimal | Risk threshold percentage (0-100) | +| `linkType` | string | **Calculated field** - Type of link: `"personal"` (only personId), `"goal"` (personId + goalId), `"strategy"` (personId + goalId + strategyId) | +| `weight` | decimal | Relative importance (for weighted calculations, 0.0-1.0) | +| `displayOrder` | int | Sort order in UI (lower = first) | +| `isPrimary` | boolean | Is this the primary Measure for the goal? | +| `linkedAt` | datetime | When link was created | +| `progress` | object | **Issue #527** - Progress calculation based on latest target and actual values. Includes `progressPercentage` (0-100+), `status` (on_track/at_risk/behind/no_data), `variance`, `variancePercentage`, `daysUntilTarget`, and `isOverdue`. Returns `null` if no target or actual data exists. | + +#### Error Responses + +**Status:** `400 Bad Request` +```json +{ + "success": false, + "data": null, + "error": "Invalid link ID format" +} +``` + +**Status:** `404 Not Found` +```json +{ + "success": false, + "data": null, + "error": "Measure link not found" +} +``` + +--- + +### 2. Create Measure Link + +Create a new link between a Measure and person, optionally associating with a goal and/or strategy. + +**Endpoint:** `POST /measure-links` + +#### Request Body + +```json +{ + "measureId": "measure-456e7890-e89b-12d3-a456-426614174001", + "personId": "person-789e1234-e89b-12d3-a456-426614174002", + "goalId": "goal-012e3456-e89b-12d3-a456-426614174003", + "strategyId": "strategy-345e6789-e89b-12d3-a456-426614174004", + "thresholdPct": 85.0, + "riskThresholdPct": 50.0, + "weight": 1.5, + "displayOrder": 1, + "isPrimary": false +} +``` + +#### Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `measureId` | string (GUID) | **Yes** | Measure to link | +| `personId` | string (GUID) | **Yes** | Person responsible/owner | +| `goalId` | string (GUID) | No | Goal to associate (required if strategyId provided) | +| `strategyId` | string (GUID) | No | Strategy to associate (requires goalId) | +| `thresholdPct` | decimal | No | Threshold percentage (0-100, default: 80.0) | +| `riskThresholdPct` | decimal | No | Risk threshold percentage (0-100, default: 50.0) | +| `weight` | decimal | No | Relative weight (default: 1.0) | +| `displayOrder` | int | No | Sort order (default: auto-assigned) | +| `isPrimary` | boolean | No | Mark as primary Measure (default: false) | + +**Note:** `linkType` is NOT accepted in requests - it is automatically calculated based on provided fields. + +#### Response + +**Status:** `201 Created` + +```json +{ + "success": true, + "data": { + "id": "link-new-123", + "measureId": "measure-456e7890-e89b-12d3-a456-426614174001", + "personId": "person-789e1234-e89b-12d3-a456-426614174002", + "goalId": "goal-012e3456-e89b-12d3-a456-426614174003", + "strategyId": "strategy-345e6789-e89b-12d3-a456-426614174004", + "thresholdPct": 85.0, + "riskThresholdPct": 50.0, + "linkType": "strategy", + "weight": 0.5, + "displayOrder": 1, + "isPrimary": false, + "linkedAt": "2025-12-26T10:00:00Z" + }, + "error": null +} +``` + +#### Business Rules + +- **Person Required:** PersonId must always be provided +- **Strategy Requires Goal:** If strategyId is provided, goalId must also be provided +- **Entity Validation:** All referenced entities (Measure, person, goal, strategy) must exist in tenant +- **Duplicate Prevention:** Cannot create duplicate links (same Measure + person + goal + strategy combination) +- **Auto linkType:** Calculated automatically: `personal` (no goal/strategy), `goal` (has goalId), `strategy` (has goalId + strategyId) +- **Primary Measure:** Setting isPrimary=true may unset other primary Measures for the same goal + +--- + +### 3. Update Measure Link + +Update link configuration (person, goal, strategy, threshold, weight, priority, etc.). + +**Endpoint:** `PUT /measure-links/{linkId}` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `linkId` | string (GUID) | **Yes** | Measure link identifier | + +#### Request Body + +```json +{ + "personId": "person-789e1234-e89b-12d3-a456-426614174002", + "goalId": "goal-012e3456-e89b-12d3-a456-426614174003", + "strategyId": "strategy-345e6789-e89b-12d3-a456-426614174004", + "thresholdPct": 85.0, + "riskThresholdPct": 50.0, + "weight": 2.0, + "displayOrder": 1, + "isPrimary": true +} +``` + +#### Request Fields + +All fields are optional. Only provided fields will be updated. + +| Field | Type | Description | +|-------|------|-------------| +| `personId` | string (GUID) | Person responsible for this link. **When changed, propagates to the Measure and all other links for that Measure.** | +| `goalId` | string (GUID) or null | Goal association. Omitted = no change; value = set/change goal; explicit null = remove goal. When goal is explicitly removed and `strategyId` is omitted, strategy is auto-removed. | +| `strategyId` | string (GUID) or null | Strategy association. Omitted = no change; value = set/change strategy; explicit null = remove strategy. Requires a goal to be set after update. | +| `thresholdPct` | decimal | Threshold percentage (0-100) | +| `riskThresholdPct` | decimal | Risk threshold percentage (0-100) | +| `weight` | decimal | Relative weight/importance | +| `displayOrder` | int | Sort order in UI | +| `isPrimary` | boolean | Mark as primary Measure | + +**Notes:** +- You cannot change `measureId` via update. Delete and recreate the link instead. +- `linkType` is a calculated field and cannot be provided in requests. It is automatically derived from the presence of `goalId` and `strategyId`. +- **PersonId Propagation:** When `personId` is updated, the change propagates to: + - The Measure's `ownerId` field + - All other MeasureLinks associated with the same Measure +- **Strategy Validation:** When `strategyId` is provided, the system validates that: + - A `goalId` is set (either in the request or already on the link) + - The strategy exists and belongs to the specified goal +- **Null vs Omitted Semantics (Issue #663):** For `goalId` and `strategyId` updates: + - **Omitted field**: existing value is preserved + - **Provided value**: association is updated + - **Provided as `null`**: association is explicitly removed + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "id": "link-123e4567-e89b-12d3-a456-426614174000", + "measureId": "measure-456e7890-e89b-12d3-a456-426614174001", + "personId": "person-789e1234-e89b-12d3-a456-426614174002", + "goalId": "goal-012e3456-e89b-12d3-a456-426614174003", + "strategyId": "strategy-345e6789-e89b-12d3-a456-426614174004", + "thresholdPct": 85.0, + "riskThresholdPct": 50.0, + "linkType": "strategy", + "weight": 0.8, + "displayOrder": 1, + "isPrimary": true, + "linkedAt": "2025-12-20T10:00:00Z" + }, + "error": null +} +``` + +#### Business Rules + +- **Partial Updates:** Only provided fields are updated +- **PersonId Propagation:** Updating `personId` cascades to Measure and all related links atomically +- **Goal Association:** Can be set or changed; removing goal also removes strategy +- **Strategy Validation:** + - Strategy requires a goal to be present + - Strategy must belong to the specified goal + - System validates strategy-goal relationship before update +- **Primary Measure:** Setting `isPrimary: true` may unset other primary Measures for the same goal +- **Threshold Range:** Must be between 0 and 100 if provided +- **Risk Threshold Range:** Must be between 0 and 100 if provided and must be <= thresholdPct +- **Display Order:** Used for UI sorting, can be any positive integer +- **Calculated linkType:** Automatically derived from `goalId` and `strategyId` presence - cannot be set via API + +#### Error Responses + +**404 Not Found** - Link, Goal, or Strategy not found: +```json +{ + "success": false, + "error": "MEASURE link not found", + "code": "MEASURE_LINK_NOT_FOUND" +} +``` + +**400 Bad Request** - Validation errors: +```json +{ + "success": false, + "error": "Strategy requires a Goal to be set", + "code": "STRATEGY_REQUIRES_GOAL" +} +``` + +```json +{ + "success": false, + "error": "Strategy 345e6789... does not belong to Goal 012e3456...", + "code": "STRATEGY_GOAL_MISMATCH" +} +``` + +--- + +### 4. Delete Measure Link + +Remove a Measure link. + +**Endpoint:** `DELETE /measure-links/{linkId}` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `linkId` | string (GUID) | **Yes** | Measure link identifier | + +#### Request Example + +```http +DELETE /measure-links/link-123e4567-e89b-12d3-a456-426614174000 +Authorization: Bearer {token} +X-Tenant-Id: {tenantId} +``` + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "message": "Measure link deleted successfully", + "warning": null + }, + "error": null +} +``` + +**Status:** `200 OK` (with warning when last link) + +```json +{ + "success": true, + "data": { + "message": "Measure link deleted successfully", + "warning": "This was the last link for Measure 'Customer Satisfaction Score'. The Measure is now orphaned and cannot be managed. Consider deleting the Measure if no longer needed." + }, + "error": null +} +``` + +#### Business Rules + +- **No Cascade:** Does not delete the Measure, goal, or any related data +- **Orphaned Measure Warning:** If this is the last link for a tenant-specific Measure, a warning is returned suggesting Measure deletion +- **Primary Status:** If deleted link was primary, goal has no primary Measure until another is set +- **Targets/Actuals Preserved:** Measure target and actual data remain intact (not deleted with link) + +--- + +### 5. Query Measure Links + +Get Measure links with flexible filtering by Measure, person, goal, or strategy. + +**Endpoint:** `GET /measure-links` + +#### Query Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `measureId` | string (GUID) | No | Filter by Measure | +| `personId` | string (GUID) | No | Filter by person | +| `goalId` | string (GUID) | No | Filter by goal | +| `strategyId` | string (GUID) | No | Filter by strategy | +| `includeAll` | boolean | No | Include nested links (default: false) | +| `measureInfo` | boolean | No | Include enriched measure details (default: false) - **Issue #562** | + +**At least one filter parameter is required.** Multiple parameters can be combined. + +#### Query Behaviors + +**By Measure:** +```http +GET /measure-links?measureId={measureId} +``` +Returns all links for this Measure (all persons/goals/strategies where it's used). + +**By Person (personal Measures only):** +```http +GET /measure-links?personId={personId} +``` +Returns only personal Measures for this person (no goal or strategy associations). + +**By Person (all Measures):** +```http +GET /measure-links?personId={personId}&includeAll=true +``` +Returns all Measures assigned to this person (personal + goal + strategy). + +**By Goal (goal-level Measures only):** +```http +GET /measure-links?goalId={goalId} +``` +Returns only goal-level Measures (no strategy associations). + +**By Goal (all Measures including strategies):** +```http +GET /measure-links?goalId={goalId}&includeAll=true +``` +Returns goal-level Measures AND strategy Measures for this goal. + +**By Strategy:** +```http +GET /measure-links?strategyId={strategyId} +``` +Returns all Measures linked to this strategy. + +**By Person + Goal (intersection):** +```http +GET /measure-links?personId={personId}&goalId={goalId} +``` +Returns Measures where this person is assigned to work on this specific goal. + +**With Enriched Measure Info (Issue #562):** +```http +GET /measure-links?goalId={goalId}&measureInfo=true +``` +Returns measure links with additional measure details including aggregation config, owner info, trend data, and metadata. + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": [ + { + "id": "link-001", + "measureId": "measure-456e7890-e89b-12d3-a456-426614174001", + "measureName": "Customer Satisfaction Score", + "unit": "percentage", + "personId": "person-123", + "personName": "John Doe", + "goalId": "goal-001", + "goalTitle": "Increase Customer Satisfaction", + "strategyId": "strategy-001", + "strategyTitle": "Improve Support Response Time", + "thresholdPct": 80.0, + "linkType": "strategy", + "weight": 0.75, + "displayOrder": 1, + "isPrimary": true, + "linkedAt": "2025-12-20T10:00:00Z", + "currentValue": 87.5, + "currentValueDate": "2025-12-25T09:30:00Z", + "progress": { + "progressPercentage": 85.5, + "status": "on_track", + "variance": 10.5, + "variancePercentage": 14.0, + "daysUntilTarget": 15, + "isOverdue": false + }, + "measure": { + "measurementConfig": { + "aggregationPeriod": "monthly" + }, + "ownerId": "person-789", + "ownerName": "Sarah Johnson", + "ownerEmail": "sarah.johnson@example.com", + "trendData": [ + { + "date": "2025-12-01", + "value": 85.0, + "isTarget": true + }, + { + "date": "2025-12-01", + "value": 82.3, + "isTarget": false + }, + { + "date": "2025-11-01", + "value": 80.0, + "isTarget": true + }, + { + "date": "2025-11-01", + "value": 78.5, + "isTarget": false + } + ], + "metadata": { + "createdAt": "2025-10-15T08:30:00Z", + "updatedAt": "2025-12-20T14:22:00Z" + } + } + }, + { + "id": "link-002", + "measureId": "measure-456e7890-e89b-12d3-a456-426614174001", + "measureName": "Customer Satisfaction Score", + "unit": "percentage", + "personId": "person-456", + "personName": "Jane Smith", + "goalId": "goal-002", + "strategyId": null, + "thresholdPct": 75.0, + "linkType": "goal", + "weight": 0.5, + "displayOrder": 2, + "isPrimary": false, + "linkedAt": "2025-12-21T14:30:00Z", + "currentValue": null, + "currentValueDate": null + } + ], + "error": null +} +``` + +**Note:** The query response includes denormalized fields for convenience: +- `measureName` - Name of the linked Measure +- `unit` - Unit of measurement +- `personName` - Name of the responsible person +- `goalTitle` - Title/name of the linked goal (if any) - **Issue #569** +- `strategyTitle` - Title/name of the linked strategy (if any) - **Issue #569** +- `currentValue` - Latest actual value (if any) +- `currentValueDate` - Date of the latest actual value (if any) +- `progress` - Progress calculation (Issue #527) +- `measure` - Enriched measure details (only when `measureInfo=true`) - **Issue #562** + - `measurementConfig.aggregationPeriod` - Aggregation period ('daily', 'weekly', 'monthly', 'quarterly', 'yearly') + - `ownerId` - Person responsible for the measure + - `ownerName` - Owner's display name (denormalized) + - `ownerEmail` - Owner's email (denormalized) + - `trendData` - Array of recent data points (5-10 points) for sparkline visualization + - `metadata` - Audit information (createdAt, updatedAt) + +#### Use Cases + +- **Personal scorecards:** `?personId={id}` - Show person's personal Measures +- **Person workload:** `?personId={id}&includeAll=true` - All Measures assigned to person +- **Goal tracking:** `?goalId={id}` - Goal-level Measures only +- **Goal cascade:** `?goalId={id}&includeAll=true` - Goal + strategy Measures +- **Strategy metrics:** `?strategyId={id}` - All Measures for a strategy +- **Cross-functional Measures:** `?measureId={id}` - Where is this Measure used? +- **Person-goal assignment:** `?personId={id}&goalId={id}` - Person's work on specific goal + +--- + +## Cascade Delete Behavior + +When a tenant-specific Measure is deleted via `DELETE /measures/{measureId}`, the following cascade deletions occur automatically: + +1. **Measure Links:** All links to this Measure are deleted +2. **Measure Targets:** All target data for this Measure is deleted +3. **Measure Actuals:** All actual measurement data for this Measure is deleted + +**Note:** This cascade only applies to tenant-specific Measures. Catalog Measures cannot be deleted by users. + +**Warning:** Cascade delete is permanent and cannot be undone. Ensure proper confirmation before deleting Measures. + +--- + +## Data Models + +### Link Type (Calculated Field) + +```typescript +type LinkType = "personal" | "goal" | "strategy"; +``` + +**Calculation Logic:** +- **personal:** Only `personId` is set (no `goalId` or `strategyId`) +- **goal:** `personId` + `goalId` are set (no `strategyId`) +- **strategy:** `personId` + `goalId` + `strategyId` are all set + +**Important:** `linkType` is NOT persisted in the database. It is calculated on-the-fly based on the presence of foreign keys. Do not include it in POST/PUT request bodies. + +### Threshold Percentage + +- **Range:** 0 - 100 +- **Meaning:** Completion threshold for the Measure (e.g., 80% = "on track" at 80% of target) +- **Usage:** UI can show red/yellow/green status based on `currentValue` vs `targetValue` at threshold + +### Risk Threshold Percentage + +- **Range:** 0 - 100 (must be ≤ `thresholdPct`) +- **Meaning:** Risk threshold for "at risk" status (e.g., 50% = "at risk" when variance percentage < -50) +- **Usage:** Determines split between "behind" and "at_risk" statuses + +### Weight + +- **Range:** 0.0 - 1.0 +- **Meaning:** Relative importance for weighted calculations (normalized) +- **Default:** 1.0 (but must be within 0.0-1.0 range) +- **Validation:** Domain enforces 0.0 ≤ weight ≤ 1.0 +- **Usage:** When calculating weighted averages of multiple Measures + +### Display Order + +- **Range:** Any positive integer +- **Meaning:** Sort order in UI (ascending) +- **Usage:** Controls how Measures are listed on scorecards/dashboards + +--- + +## Business Rules + +### Link Creation + +1. **Entity Validation:** All referenced entities (Measure, person, goal, strategy) must exist in tenant +2. **Duplicate Prevention:** Cannot create duplicate links (same Measure + same person + same goal + same strategy) +3. **Strategy Requires Goal:** Cannot link to strategy without also linking to goal +4. **Person Required:** Every link must have a person (owner) + +### Primary Measure + +1. **Goal Scope:** Primary flag only applies when goalId is present +2. **Single Primary:** Only one Measure can be primary per goal +3. **Auto-Unset:** Setting isPrimary=true on one link may automatically set isPrimary=false on other links for the same goal + +### Delete Behavior + +1. **Link Deletion:** Removing a link does NOT delete the Measure or any entities +2. **Orphan Warning:** If last link is deleted for a tenant-specific Measure, system warns about orphaned Measure +3. **Cascade Delete:** Only `DELETE /measures/{id}` triggers cascade (links, targets, actuals) + +--- + +## Error Handling + +### Standard Error Response + +```json +{ + "success": false, + "data": null, + "error": "Error message describing what went wrong" +} +``` + +### Common Error Codes + +| Status Code | Scenario | Error Message | +|-------------|----------|---------------| +| `400` | Invalid GUID format | "Invalid link ID format" | +| `400` | Missing required field | "PersonId is required" | +| `400` | Strategy without goal | "Strategy link requires goalId" | +| `400` | Duplicate link | "Measure link already exists" | +| `404` | Link not found | "Measure link not found" | +| `404` | Referenced entity not found | "Measure not found" | +| `422` | Validation failed | "Threshold percentage must be between 0 and 100" | + +--- + +## Frontend Usage Examples + +### TypeScript Service + +```typescript +// Create personal Measure link +const response = await traction.post('/measure-links', { + measureId: 'measure-123', + personId: 'person-456', + thresholdPct: 85.0, + weight: 0.8, // Must be between 0.0 and 1.0 + displayOrder: 1 +}); + +// Create goal Measure link +await traction.post('/measure-links', { + measureId: 'measure-123', + personId: 'person-456', + goalId: 'goal-789', + thresholdPct: 80.0, + weight: 0.9, // Must be between 0.0 and 1.0 + isPrimary: true +}); + +// Create strategy Measure link +await traction.post('/measure-links', { + measureId: 'measure-123', + personId: 'person-456', + goalId: 'goal-789', + strategyId: 'strategy-101', + thresholdPct: 75.0, + weight: 0.7 // Must be between 0.0 and 1.0 +}); + +// Query personal Measures (returns denormalized fields) +const personalMeasures = await traction.get('/measure-links', { + params: { personId: 'person-456' } +}); +// Response includes: measureName, unit, personName, currentValue, currentValueDate + +// Query all Measures for person (including goals/strategies) +const allMeasures = await traction.get('/measure-links', { + params: { personId: 'person-456', includeAll: true } +}); + +// Query goal Measures +const goalMeasures = await traction.get('/measure-links', { + params: { goalId: 'goal-789' } +}); + +// Query goal + strategy Measures +const goalAllMeasures = await traction.get('/measure-links', { + params: { goalId: 'goal-789', includeAll: true } +}); + +// Update link configuration +await traction.put(`/measure-links/${linkId}`, { + thresholdPct: 90.0, + isPrimary: true, + weight: 0.95 // Must be between 0.0 and 1.0 +}); + +// Delete link +await traction.delete(`/measure-links/${linkId}`); +``` + +--- + +## Related APIs + +- **[Measures API](./measures-api.md)** - Manage Measures +- **[Measure Data API](./measure-data-api.md)** - Record Measure values, targets, actuals +- **[Goals API](./goals-api.md)** - Manage goals +- **[Strategies API](./strategies-api.md)** - Manage strategies +- **[People API](./people-api.md)** - Manage people/team members + +--- + +## Changelog + +### v8.3 (January 15, 2026) - Issue #569: Goal and Strategy Titles +- ✨ **Added:** `goalTitle` and `strategyTitle` fields to all Measure Link response payloads +- 🚀 **Performance:** Reduces frontend API calls by providing denormalized goal/strategy names directly in responses +- 📝 **Implementation:** Query handler fetches and populates titles with throttled parallelism (max 10 concurrent) +- 🔧 **Nullable fields:** Both fields return `null` when goal/strategy is not linked to the Measure Link + +### v8.2 (January 10, 2026) - Issue #527: Progress Calculation +- ✨ **Added:** `progress` field to all measure link responses (GET /measure-links, GET /measure-links/{id}) +- 📊 Progress calculation includes: progressPercentage, status (on_track/at_risk/behind/no_data), variance, variancePercentage, daysUntilTarget, isOverdue +- 🧮 Implemented via domain service pattern (`MeasureLinkProgressService`) following DDD principles +- 📝 Progress calculated based on latest target and actual values, accounting for measure direction and threshold +- 🚀 Batch optimization with throttled parallelism (max 10 concurrent) for performance + +### v8.1 (January 8, 2026) +- 📝 **Updated:** Weight range clarified to 0.0-1.0 (matches domain validation) +- ✨ **Added:** Query response includes denormalized fields: `measureName`, `unit`, `personName`, `currentValue`, `currentValueDate` +- 📝 **Documentation:** Updated all examples to reflect actual implementation with denormalized fields +- 🔧 **Clarification:** Weight validation enforces 0.0 ≤ weight ≤ 1.0 in domain layer + +### v8.0 (December 26, 2025) +- 🔄 **BREAKING:** Unified Measure Links API - removed separate controllers +- ❌ **Removed:** `PersonalMeasuresController` endpoints (`POST /people/{personId}/measures:link`, `POST /people/{personId}/measures:unlink`, `GET /people/{personId}/measures`) +- ❌ **Removed:** `StrategyMeasuresController` endpoints (`GET /strategies/{strategyId}/measures`) +- ✨ **Added:** `POST /measure-links` - unified endpoint for creating all link types +- ✨ **Added:** `GET /measure-links` with query parameters - flexible filtering by measureId, personId, goalId, strategyId +- ✨ **Added:** `includeAll` query parameter for nested link retrieval +- 🔄 **Changed:** `linkType` is now a calculated/derived field (not persisted, not accepted in requests) +- ✨ **Added:** Cascade delete behavior when Measures are deleted +- ✨ **Added:** Orphaned Measure warning when last link is deleted +- 📝 **Documentation:** Complete v8 specification with 5 unified endpoints + +### v7.0 (December 23, 2025) +- ✨ New MeasureLink design replacing GoalMeasureLink (Issue #374) +- ✅ Documented 8 endpoints across 3 controllers +- ✨ Added personal scorecard endpoints (`/people/{personId}/measures`) +- ✨ Added strategy Measure endpoints (`/strategies/{strategyId}/measures`) + +--- + +**[← Back to Traction Service Index](./README.md)** diff --git a/docs/shared/Specifications/user-app/traction-service/measures-api.md b/docs/shared/Specifications/user-app/traction-service/measures-api.md new file mode 100644 index 00000000..a09812c0 --- /dev/null +++ b/docs/shared/Specifications/user-app/traction-service/measures-api.md @@ -0,0 +1,1440 @@ +# Measures API Specification + +**Version:** 7.3 +**Last Updated:** January 9, 2026 +**Base Path:** `/measures` +**Controller:** `MeasuresController.cs` + +## Overview + +The Measures API manages Key Performance Indicators (Measures) within the PurposePath system. Measures measure progress toward strategies and goals, supporting both catalog-based Measures (from the Measure library) and custom user-defined Measures. + +### Key Features +- List Measures with filtering by owner, goal, or strategy +- Create catalog-based or custom Measures +- Update Measure details or current values +- Soft delete Measures (preserves historical data) +- Query Measure-goal relationships +- Retrieve Measures catalog for designing a new goal (no goalId required) +- Retrieve Measure catalog for new (unpersisted) goals + +--- + +## Authentication + +All endpoints require: +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +--- + +## Endpoints + +### 0. Get Measure Summary + +**NEW** Issue #526 + +Retrieve comprehensive MEASURE summary with filtering, aggregations, and detailed measure data. This endpoint provides a complete view of all measures with their relationships, progress, and summary statistics. + +**Endpoint:** `GET /measures/summary` + +**Headers:** +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Query Parameters:** +- `category` (optional): Filter by measure category (e.g., "Finance", "Sales", "Customer Success") +- `ownerId` (optional): Filter by owner (person ID) +- `status` (optional): Filter by measure status ("active" or "inactive") +- `progressStatus` (optional): Filter by progress status ("on_track", "at_risk", "behind", "no_data") +- `period` (optional): Filter by aggregation period ("daily", "weekly", "monthly", "quarterly", "yearly") +- `includeInactive` (optional): Include inactive measures (default: true) +- `maxDataPoints` (optional): Maximum trend data points to return (default: 5, 0 = all) + +**Response 200** + +```json +{ + "success": true, + "data": { + "measures": [ + { + "measureId": "m-001", + "measureName": "Monthly Recurring Revenue", + "description": "Total monthly recurring revenue from all subscriptions", + "unit": "$", + "direction": "up", + "type": "quantitative", + "category": "Finance", + + "currentValue": 125000, + "currentValueDate": "2025-01-08", + + "latestTarget": { + "targetId": "t-001", + "targetValue": 150000, + "targetDate": "2025-01-31", + "optimalValue": 175000, + "minimalValue": 140000, + "label": "Q1 Target" + }, + + "latestActual": { + "actualId": "a-001", + "actualValue": 125000, + "measurementDate": "2025-01-08", + "actualSubtype": "Measured", + "recordedBy": "user-123", + "recordedByName": "John Smith" + }, + + "progress": { + "progressPercentage": 83.3, + "status": "on_track", + "variance": -25000, + "variancePercentage": -16.7, + "daysUntilTarget": 23, + "isOverdue": false + }, + + "owner": { + "ownerId": "person-456", + "ownerName": "Jane Doe", + "ownerEmail": "jane.doe@example.com" + }, + + "goalLinks": [ + { + "linkId": "link-001", + "goalId": "goal-001", + "goalTitle": "Increase Revenue", + "goalIntent": "Grow monthly recurring revenue to support expansion", + "goalStatus": "active", + "isPrimary": true, + "thresholdPct": 80, + "riskThresholdPct": 50, + "weight": 1.0, + "displayOrder": 0, + "linkedAt": "2025-01-01T00:00:00Z", + "linkOwner": { + "personId": "person-456", + "personName": "Jane Doe" + }, + "progress": { + "progressPercentage": 83.3, + "status": "on_track", + "variance": -25000, + "variancePercentage": -16.7, + "daysUntilTarget": 23, + "isOverdue": false + } + } + ], + + "strategyLinks": [ + { + "linkId": "link-003", + "strategyId": "strat-001", + "strategyTitle": "Enterprise Sales Initiative", + "strategyDescription": "Focus on closing enterprise deals", + "strategyStatus": "in_progress", + "goalId": "goal-001", + "goalTitle": "Increase Revenue", + "thresholdPct": 85, + "riskThresholdPct": 50, + "weight": 0.7, + "displayOrder": 0, + "linkedAt": "2025-01-02T00:00:00Z", + "linkOwner": { + "personId": "person-456", + "personName": "Jane Doe" + }, + "progress": { + "progressPercentage": 83.3, + "status": "on_track", + "variance": -25000, + "variancePercentage": -16.7, + "daysUntilTarget": 23, + "isOverdue": false + } + } + ], + + "measurementConfig": { + "aggregationType": "sum", + "aggregationPeriod": "monthly", + "valueType": "aggregate", + "interpolationMethod": "linear", + "measurementFrequency": "daily" + }, + + "sharingInfo": { + "isShared": true, + "totalGoalLinks": 2, + "totalStrategyLinks": 1, + "catalogId": null, + "isCustom": true + }, + + "metadata": { + "createdAt": "2024-12-15T10:00:00Z", + "updatedAt": "2025-01-08T14:30:00Z", + "createdBy": "user-456" + }, + + "trendData": [ + { + "date": "2024-12-01", + "value": 100000, + "isTarget": false, + "isEstimate": false + }, + { + "date": "2024-12-15", + "value": 110000, + "isTarget": false, + "isEstimate": false + }, + { + "date": "2025-01-01", + "value": 120000, + "isTarget": false, + "isEstimate": false + }, + { + "date": "2025-01-08", + "value": 125000, + "isTarget": false, + "isEstimate": false + }, + { + "date": "2025-01-31", + "value": 150000, + "isTarget": true, + "isEstimate": false + } + ] + } + ], + + "summary": { + "totalMeasures": 47, + "totalActiveMeasures": 42, + "totalInactiveMeasures": 5, + + "statusBreakdown": { + "onTrack": 28, + "atRisk": 10, + "behind": 4, + "noData": 5 + }, + + "categoryBreakdown": [ + { + "category": "Finance", + "count": 12, + "onTrack": 8, + "atRisk": 3, + "behind": 1 + }, + { + "category": "Sales", + "count": 10, + "onTrack": 7, + "atRisk": 2, + "behind": 1 + }, + { + "category": "Customer Success", + "count": 8, + "onTrack": 5, + "atRisk": 2, + "behind": 1 + } + ], + + "ownerBreakdown": [ + { + "ownerId": "person-456", + "ownerName": "Jane Doe", + "measureCount": 8, + "onTrackCount": 6 + }, + { + "ownerId": "person-789", + "ownerName": "Bob Johnson", + "measureCount": 5, + "onTrackCount": 4 + } + ], + + "overallHealthScore": 73.5 + }, + + "metadata": { + "generatedAt": "2025-01-09T10:30:45Z", + "queryDurationMs": 124, + "filters": { + "category": null, + "ownerId": null, + "status": null, + "progressStatus": null, + "period": null, + "includeInactive": true, + "maxDataPoints": 5 + }, + "version": "1.0" + } + }, + "error": null, + "timestamp": "2025-01-09T10:30:45Z" +} +``` + +**Progress Status Calculation** + +The `progress.status` field is calculated using the following algorithm: + +``` +Given: +- thresholdPct (e.g., 80) +- riskThresholdPct (e.g., 50) +- variancePercentage = ((currentValue - expectedValue) / |totalChange|) * 100 + +Status determination: +- If variancePercentage >= (thresholdPct - 100) -> on_track +- If variancePercentage < (thresholdPct - 100) AND variancePercentage >= (riskThresholdPct - 100) -> behind +- If variancePercentage < (riskThresholdPct - 100) -> at_risk +- No data -> no_data + +Example with thresholdPct = 80, riskThresholdPct = 50: +- variancePercentage >= -20 -> on_track +- -50 <= variancePercentage < -20 -> behind +- variancePercentage < -50 -> at_risk +``` + +**Response 400 Bad Request** + +```json +{ + "success": false, + "data": null, + "error": "Invalid filter parameters", + "timestamp": "2025-01-09T10:30:45Z" +} +``` + +**Response 401 Unauthorized** + +```json +{ + "success": false, + "data": null, + "error": "Unauthorized - Invalid or expired token", + "timestamp": "2025-01-09T10:30:45Z" +} +``` + +**Response 500 Internal Server Error** + +```json +{ + "success": false, + "data": null, + "error": "Internal server error", + "timestamp": "2025-01-09T10:30:45Z" +} +``` + +**Implementation:** +- Controller: `MeasuresController.GetMeasureSummary()` +- Handler: `GetMeasureSummaryQueryHandler` +- Query: `GetMeasureSummaryQuery` +- Response DTOs: `MeasureSummaryResponse`, `MeasureSummaryItemResponse`, etc. + +--- + +### 0a. Get Available Measures For New Goal (No goalId) + +Retrieve the Measures catalog (catalog + tenant custom) when designing a goal that is not yet persisted. This mirrors the goal-scoped available-measures payload but does not require a `goalId`; `usageInfo.isUsedByThisGoal` is always `false`. + +**Endpoint:** `GET /goals/available-measures` + +**Headers:** +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +**Response 200** + +```json +{ + "success": true, + "data": { + "catalogMeasures": [ + { + "id": "catalog-001", + "name": "Monthly Recurring Revenue", + "description": "Total predictable revenue from subscriptions", + "category": "Financial", + "unit": "USD", + "direction": "increase", + "type": "leading", + "valueType": "currency", + "aggregationType": "sum", + "aggregationPeriod": "monthly", + "calculationMethod": "Sum of all active subscription values", + "isIntegrationEnabled": true, + "usageInfo": { + "goalCount": 3, + "isUsedByThisGoal": false + } + } + ], + "tenantCustomMeasures": [ + { + "id": "custom-measure-001", + "name": "Customer Satisfaction Score", + "description": "Average CSAT from post-purchase surveys", + "category": "Customer Experience", + "unit": "score", + "direction": "increase", + "type": "lagging", + "valueType": "percentage", + "aggregationType": "average", + "aggregationPeriod": "monthly", + "calculationMethod": "Average of all survey responses", + "measureCatalogId": null, + "isIntegrationEnabled": false, + "createdAt": "2025-01-15T10:00:00.000Z", + "createdBy": "user-123", + "usageInfo": { + "goalCount": 1, + "isUsedByThisGoal": false + } + } + ] + }, + "error": null, + "timestamp": "2025-12-23T11:30:00.000Z" +} +``` + +**Notes** +- Same schema as goal-scoped available-measures but without validating goal existence. +- `usageInfo.isUsedByThisGoal` is always `false` for this endpoint; `goalCount` still reflects usage across persisted goals. +- Use [Measure Links API](./measure-links-api.md) to link a Measure once the goal is created. + +### 1. List Measures + +Retrieve Measures with optional filtering. + +**Endpoint:** `GET /measures` + +#### Query Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `ownerId` | string (GUID) | No | Filter by Measure owner (person responsible) | +| `goalId` | string (GUID) | No | Filter by linked goal | +| `strategyId` | string (GUID) | No | Filter by linked strategy | + +**Default Behavior:** If no filter is provided, returns all Measures for the tenant (useful for dropdowns and lists). + +#### Request Example + +```http +GET /measures?goalId=550e8400-e29b-41d4-a716-446655440000 +Authorization: Bearer {token} +X-Tenant-Id: {tenantId} +``` + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "data": [ + { + "id": "123e4567-e89b-12d3-a456-426614174000", + "tenantId": "tenant-123", + "name": "Monthly Recurring Revenue", + "description": "Total MRR from all active subscriptions", + "currentValue": 45000.00, + "targetValue": 50000.00, + "unit": "USD", + "direction": "Increase", + "type": "Leading", + "category": "Finance", + "measurementFrequency": "Monthly", + "dataSource": "Stripe API", + "catalogId": "measure-catalog-001", + "ownerId": "owner-123", + "strategyId": "strategy-456", + "currentValueDate": "2025-12-15T00:00:00Z", + "createdAt": "2025-01-15T10:30:00Z", + "updatedAt": "2025-12-15T14:25:00Z", + "isDeleted": false + } + ], + "pagination": { + "totalCount": 1, + "page": 1, + "pageSize": 50 + } + }, + "error": null +} +``` + +#### Field Descriptions + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string (GUID) | Unique Measure identifier | +| `tenantId` | string | Organization identifier | +| `name` | string | Measure name (max 200 chars) | +| `description` | string | Detailed description (max 1000 chars) | +| `currentValue` | decimal | Latest recorded value | +| `targetValue` | decimal | Goal/target value | +| `unit` | string | Measurement unit (e.g., "USD", "%", "count") | +| `direction` | enum | `Increase` or `Decrease` (desired trend) | +| `type` | enum | `Leading` or `Lagging` indicator | +| `category` | string | Measure category (e.g., "Finance", "Sales", "Operations") | +| `measurementFrequency` | string | How often measured (e.g., "Daily", "Weekly", "Monthly") | +| `dataSource` | string | Where data comes from | +| `catalogId` | string | If from Measure library, the catalog entry ID | +| `ownerId` | string (GUID) | Person responsible for this Measure | +| `strategyId` | string (GUID) | Linked strategy | +| `currentValueDate` | datetime | When current value was recorded | +| `createdAt` | datetime | When Measure was created | +| `updatedAt` | datetime | Last update timestamp | +| `isDeleted` | boolean | Soft delete flag | + +#### Business Rules + +- **Filtering Priority:** If multiple filters are provided: + 1. `ownerId` takes precedence + 2. Then `goalId` + 3. Then `strategyId` + 4. Default: All tenant Measures +- **Multi-tenancy:** Only returns Measures for the specified tenant +- **Soft Deletes:** Deleted Measures (`isDeleted: true`) are excluded by default + +--- + +### 2. Create Measure + +Create a new Measure (catalog-based or custom). + +**Endpoint:** `POST /measures` + +#### Request Body + +```json +{ + "name": "Customer Retention Rate", + "description": "Percentage of customers retained over a period", + "currentValue": 85.5, + "targetValue": 90.0, + "unit": "%", + "direction": "Increase", + "type": "Lagging", + "category": "Customer Success", + "measurementFrequency": "Monthly", + "dataSource": "CRM System", + "ownerId": "owner-123", + "strategyId": "strategy-456", + "catalogId": "measure-catalog-002", + "aggregationType": "Average", + "aggregationPeriod": "Month", + "valueType": "Percentage", + "calculationMethod": "(Customers at End - New Customers) / Customers at Start * 100", + "currentValueDate": "2025-12-15T00:00:00Z" +} +``` + +#### Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | string | **Yes** | Measure name (max 200 chars) | +| `description` | string | No | Detailed description | +| `currentValue` | decimal | No | Initial value | +| `targetValue` | decimal | No | Goal value | +| `unit` | string | No | Measurement unit | +| `direction` | enum | No | `Increase` or `Decrease` | +| `type` | enum | No | `Leading` or `Lagging` | +| `category` | string | No | Measure category | +| `measurementFrequency` | No | No | Default: "Monthly" | +| `dataSource` | string | No | Data source identifier | +| `ownerId` | string (GUID) | No | Person responsible (defaults to creator) | +| `strategyId` | string (GUID) | No | Linked strategy | +| `catalogId` | string | No | Measure catalog entry (for library Measures) | +| `aggregationType` | string | No | How data is aggregated (e.g., "Sum", "Average") | +| `aggregationPeriod` | string | No | Time period for aggregation | +| `valueType` | string | No | Data type (e.g., "Number", "Percentage", "Currency") | +| `calculationMethod` | string | No | Formula or method for calculation | +| `currentValueDate` | datetime | No | When current value was recorded (defaults to now) | +| `goalId` | string (GUID) | No | **⚠️ Deprecated:** Link Measure to goal via `/measure-links` instead | + +#### Response + +**Status:** `201 Created` +**Location:** `/measures/{id}` + +```json +{ + "success": true, + "data": { + "id": "789e4567-e89b-12d3-a456-426614174000", + "tenantId": "tenant-123", + "name": "Customer Retention Rate", + "description": "Percentage of customers retained over a period", + "currentValue": 85.5, + "targetValue": 90.0, + "unit": "%", + "direction": "Increase", + "type": "Lagging", + "category": "Customer Success", + "measurementFrequency": "Monthly", + "dataSource": "CRM System", + "catalogId": "measure-catalog-002", + "ownerId": "owner-123", + "strategyId": "strategy-456", + "currentValueDate": "2025-12-15T00:00:00Z", + "createdAt": "2025-12-23T10:30:00Z", + "updatedAt": "2025-12-23T10:30:00Z", + "isDeleted": false + }, + "error": null +} +``` + +#### Business Rules + +- **Catalog vs Custom:** If `catalogId` is provided, Measure is linked to library entry; otherwise, it's custom +- **Owner Assignment:** If `ownerId` is not provided, defaults to the current user (creator) +- **Tenant Isolation:** Measure is automatically associated with the current tenant +- **⚠️ Goal Linking Deprecated:** The `goalId` field in request is deprecated. Use `/measure-links` endpoint to link Measures to goals + +#### Validation Rules + +- **Name:** Required, max 200 characters +- **Target Value:** Must be > 0 if provided +- **Direction:** Must be "Increase" or "Decrease" if provided +- **Type:** Must be "Leading" or "Lagging" if provided +- **Owner & Strategy IDs:** Must be valid GUIDs and exist in tenant + +--- + +### 3. Get Measure Details + +Retrieve detailed information about a specific Measure, including linked goals. + +**Endpoint:** `GET /measures/{id}` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `id` | string (GUID) | **Yes** | Measure identifier | + +#### Request Example + +```http +GET /measures/123e4567-e89b-12d3-a456-426614174000 +Authorization: Bearer {token} +X-Tenant-Id: {tenantId} +``` + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "id": "123e4567-e89b-12d3-a456-426614174000", + "tenantId": "tenant-123", + "name": "Monthly Recurring Revenue", + "description": "Total MRR from all active subscriptions", + "currentValue": 45000.00, + "targetValue": 50000.00, + "unit": "USD", + "direction": "Increase", + "type": "Leading", + "category": "Finance", + "measurementFrequency": "Monthly", + "dataSource": "Stripe API", + "catalogId": "measure-catalog-001", + "ownerId": "owner-123", + "strategyId": "strategy-456", + "currentValueDate": "2025-12-15T00:00:00Z", + "createdAt": "2025-01-15T10:30:00Z", + "updatedAt": "2025-12-15T14:25:00Z", + "isDeleted": false, + "progress": { + "progressPercent": 83.3 + }, + "linkedGoals": [ + { + "goalId": "goal-001", + "goalName": "Grow Revenue", + "linkId": "link-001", + "isPrimary": true, + "linkedAt": "2025-01-20T09:00:00Z" + } + ], + "historicalValues": [ + { + "value": 42000.00, + "recordedAt": "2025-11-15T00:00:00Z" + }, + { + "value": 45000.00, + "recordedAt": "2025-12-15T00:00:00Z" + } + ] + }, + "error": null +} +``` + +#### Additional Fields (Detail Response) + +| Field | Type | Description | +|-------|------|-------------| +| `progress` | object | Progress information (null if no links or insufficient data) | +| `progress.progressPercent` | decimal | Progress percentage (0-100+) calculated from baseline to target using primary link (or first link if no primary). Note: Status and variance are not included at measure level - those require thresholds which are at the link level. | +| `linkedGoals` | array | Goals linked to this Measure | +| `linkedGoals[].goalId` | string (GUID) | Goal identifier | +| `linkedGoals[].goalName` | string | Goal name | +| `linkedGoals[].linkId` | string (GUID) | Measure link identifier | +| `linkedGoals[].isPrimary` | boolean | Is this the primary Measure for the goal? | +| `linkedGoals[].linkedAt` | datetime | When link was created | +| `historicalValues` | array | Previous Measure values (last 12) | +| `historicalValues[].value` | decimal | Historical value | +| `historicalValues[].recordedAt` | datetime | When value was recorded | + +#### Error Responses + +**Status:** `400 Bad Request` +```json +{ + "success": false, + "data": null, + "error": "Invalid Measure ID format" +} +``` + +**Status:** `404 Not Found` +```json +{ + "success": false, + "data": null, + "error": "Measure not found" +} +``` + +--- + +### 4. Update Measure + +Update Measure details (name, description, target, etc.). Does not update current value — use `PUT /measures/{id}/value` for that. + +**Endpoint:** `PUT /measures/{id}` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `id` | string (GUID) | **Yes** | Measure identifier | + +#### Request Body + +```json +{ + "name": "Monthly Recurring Revenue (Updated)", + "description": "Total MRR from all active subscriptions - Updated methodology", + "targetValue": 55000.00, + "unit": "USD", + "direction": "Increase", + "type": "Leading", + "category": "Finance", + "measurementFrequency": "Monthly", + "dataSource": "Stripe API v2", + "ownerId": "new-owner-123" +} +``` + +#### Request Fields + +All fields are optional. Only provided fields will be updated. + +| Field | Type | Description | +|-------|------|-------------| +| `name` | string | Measure name (max 200 chars) | +| `description` | string | Detailed description | +| `targetValue` | decimal | Updated target value | +| `unit` | string | Measurement unit | +| `direction` | enum | `Increase` or `Decrease` | +| `type` | enum | `Leading` or `Lagging` | +| `category` | string | Measure category | +| `measurementFrequency` | string | Measurement frequency | +| `dataSource` | string | Data source identifier | +| `ownerId` | string (GUID) | New owner (person responsible) | + +**Note:** `currentValue` cannot be updated via this endpoint. Use `PUT /measures/{id}/value` instead. + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "id": "123e4567-e89b-12d3-a456-426614174000", + "tenantId": "tenant-123", + "name": "Monthly Recurring Revenue (Updated)", + "description": "Total MRR from all active subscriptions - Updated methodology", + "currentValue": 45000.00, + "targetValue": 55000.00, + "unit": "USD", + "direction": "Increase", + "type": "Leading", + "category": "Finance", + "measurementFrequency": "Monthly", + "dataSource": "Stripe API v2", + "ownerId": "new-owner-123", + "updatedAt": "2025-12-23T15:45:00Z" + }, + "error": null +} +``` + +#### Business Rules + +- **Partial Updates:** Only provided fields are updated; others remain unchanged +- **Current Value:** Cannot be updated via this endpoint (use value endpoint) +- **Tenant & ID:** Cannot change Measure's tenant or ID +- **Catalog ID:** Cannot change `catalogId` after creation + +--- + +### 5. Update Measure Value + +Update only the current value of a Measure (records new measurement). + +**Endpoint:** `PUT /measures/{id}/value` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `id` | string (GUID) | **Yes** | Measure identifier | + +#### Request Body + +```json +{ + "value": 47500.00, + "recordedAt": "2025-12-23T12:00:00Z", + "notes": "End of Q4 2025" +} +``` + +#### Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `value` | decimal | **Yes** | New Measure value | +| `recordedAt` | datetime | No | When value was recorded (defaults to now) | +| `notes` | string | No | Additional context or notes | + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "id": "123e4567-e89b-12d3-a456-426614174000", + "currentValue": 47500.00, + "currentValueDate": "2025-12-23T12:00:00Z", + "updatedAt": "2025-12-23T15:50:00Z" + }, + "error": null +} +``` + +#### Business Rules + +- **Historical Tracking:** Previous value is stored in history before updating +- **Timestamp:** If `recordedAt` is not provided, uses current UTC time +- **Event Publishing:** Triggers `MeasureValueUpdated` domain event +- **Percentage Change:** System calculates change from previous value + +#### Validation Rules + +- **Value:** Must be a valid decimal number +- **Recorded At:** Cannot be in the future +- **Notes:** Max 500 characters + +--- + +### 6. Delete Measure + +Soft delete a Measure (marks as deleted, preserves historical data). + +**Endpoint:** `DELETE /measures/{id}` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `id` | string (GUID) | **Yes** | Measure identifier | + +#### Request Example + +```http +DELETE /measures/123e4567-e89b-12d3-a456-426614174000 +Authorization: Bearer {token} +X-Tenant-Id: {tenantId} +``` + +#### Response + +**Status:** `204 No Content` + +(No response body on success) + +#### Error Responses + +**Status:** `400 Bad Request` +```json +{ + "success": false, + "data": null, + "error": "Invalid Measure ID format" +} +``` + +**Status:** `404 Not Found` +```json +{ + "success": false, + "data": null, + "error": "Measure not found" +} +``` + +#### Business Rules + +- **Soft Delete:** Measure is marked as deleted (`isDeleted: true`) but not physically removed +- **Historical Data:** All historical values and links are preserved +- **List Queries:** Deleted Measures are excluded from list results by default +- **Restoration:** Can be restored by admin/support team +- **Cascade:** Measure links to goals are also soft deleted + +--- + +### 7. Get Available Measures for New Goals + +Retrieve the Measure catalog for a tenant when designing a new goal that is not yet persisted. Returns the same payload as `GET /goals/{goalId}/available-measures` but does not require a `goalId`. Usage data reflects existing goal links across the tenant; `usageInfo.isUsedByThisGoal` is always `false` because no goal is supplied. + +**Endpoint:** `GET /goals/available-measures` + +#### Response 200 + +```json +{ + "success": true, + "data": { + "catalogMeasures": [ + { + "id": "catalog-001", + "name": "Monthly Recurring Revenue", + "description": "Total predictable revenue from subscriptions", + "category": "Financial", + "unit": "USD", + "direction": "increase", + "type": "leading", + "valueType": "currency", + "aggregationType": "sum", + "aggregationPeriod": "monthly", + "calculationMethod": "Sum of all active subscription values", + "isIntegrationEnabled": true, + "usageInfo": { + "goalCount": 3, + "isUsedByThisGoal": false + } + } + ], + "tenantCustomMeasures": [ + { + "id": "custom-measure-001", + "name": "Customer Satisfaction Score", + "description": "Average CSAT from post-purchase surveys", + "category": "Customer Experience", + "unit": "score", + "direction": "increase", + "type": "lagging", + "valueType": "percentage", + "aggregationType": "average", + "aggregationPeriod": "monthly", + "calculationMethod": "Average of all survey responses", + "measureCatalogId": null, + "isIntegrationEnabled": false, + "createdAt": "2025-01-15T10:00:00.000Z", + "createdBy": "user-123", + "usageInfo": { + "goalCount": 1, + "isUsedByThisGoal": false + } + } + ] + }, + "error": null, + "timestamp": "2026-01-02T00:00:00.000Z" +} +``` + +#### Notes + +- Use when the frontend constructs a goal in-memory and needs the catalog before persisting the goal. +- Usage counts still reflect existing goal links in the tenant; `isUsedByThisGoal` remains `false`. +- Response shape matches `GET /goals/{goalId}/available-measures` for compatibility. + +--- + +### 8. Get Measure Linked Goals (Deprecated) + +⚠️ **DEPRECATED (Issue #374):** This endpoint is being migrated to the new MeasureLink design. + +**Endpoint:** `GET /measures/{id}/linked-goals` + +#### Current Behavior + +Returns `501 Not Implemented` with migration message. + +```json +{ + "success": false, + "data": null, + "error": "This endpoint is being migrated to the new MeasureLink design. Please use the new MeasureLink endpoints." +} +``` + +#### Migration Path + +Use the following endpoints instead: +- **List Measure Links:** `GET /measure-links?measureId={id}` +- **Measure Details with Links:** `GET /measures/{id}` (includes `linkedGoals` in response) + +See [Measure Links API](./measure-links-api.md) for details. + +--- + +### 9. Get Measure Options (Issue #469) + +Retrieve options for a Qualitative measure. Returns measure-owned options if they exist, otherwise returns inherited options from the linked catalog. + +**Endpoint:** `GET /measures/{id}/options` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `id` | string (GUID) | **Yes** | Measure identifier | + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "options": [ + { + "id": "opt-001", + "measureId": "measure-123", + "catalogId": null, + "numericValue": 1, + "label": "Not Met", + "description": "Performance below 50% of target", + "sortOrder": 1 + }, + { + "id": "opt-002", + "measureId": "measure-123", + "catalogId": null, + "numericValue": 2, + "label": "Partially Met", + "description": "Performance at 50-80% of target", + "sortOrder": 2 + }, + { + "id": "opt-003", + "measureId": "measure-123", + "catalogId": null, + "numericValue": 3, + "label": "Met", + "description": "Performance at or above target", + "sortOrder": 3 + } + ], + "isInherited": false, + "sourceCatalogId": null + }, + "error": null +} +``` + +#### Response Fields + +| Field | Type | Description | +|-------|------|-------------| +| `options` | array | List of option objects | +| `options[].id` | string (GUID) | Unique option identifier | +| `options[].measureId` | string (GUID) | Measure ID (null if inherited) | +| `options[].catalogId` | string (GUID) | Catalog ID (null if measure-owned) | +| `options[].numericValue` | integer | Numeric value for aggregation | +| `options[].label` | string | Display label shown to users | +| `options[].description` | string | Optional explanation | +| `options[].sortOrder` | integer | Display order | +| `isInherited` | boolean | True if options come from catalog | +| `sourceCatalogId` | string (GUID) | Catalog ID if inherited | + +#### Business Rules + +- Returns empty array for Quantitative measures +- Measure-owned options take precedence over catalog options +- Options are sorted by `sortOrder` + +--- + +### 10. Set Measure Options (Issue #469) + +Set or replace all options for a Qualitative measure. This creates measure-owned options. + +**Endpoint:** `PUT /measures/{id}/options` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `id` | string (GUID) | **Yes** | Measure identifier | + +#### Request Body + +```json +{ + "options": [ + { + "numericValue": 1, + "label": "Not Met", + "description": "Performance below 50% of target", + "sortOrder": 1 + }, + { + "numericValue": 2, + "label": "Partially Met", + "description": "Performance at 50-80% of target", + "sortOrder": 2 + }, + { + "numericValue": 3, + "label": "Met", + "description": "Performance at or above target", + "sortOrder": 3 + } + ] +} +``` + +#### Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `options` | array | **Yes** | Options to set (minimum 2) | +| `options[].numericValue` | integer | **Yes** | Unique numeric value for aggregation | +| `options[].label` | string | **Yes** | Display label (max 100 chars) | +| `options[].description` | string | No | Explanation text | +| `options[].sortOrder` | integer | No | Display order (defaults to numericValue) | + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "options": [...], + "isInherited": false, + "sourceCatalogId": null + }, + "error": null +} +``` + +#### Validation Rules + +- Minimum 2 options required +- `numericValue` must be unique within the measure +- `label` is required and max 100 characters +- Replaces all existing measure-owned options + +--- + +### 11. Delete Measure Options (Issue #469) + +Delete all measure-owned options. After deletion, the measure will inherit options from its linked catalog (if any). + +**Endpoint:** `DELETE /measures/{id}/options` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `id` | string (GUID) | **Yes** | Measure identifier | + +#### Response + +**Status:** `204 No Content` + +(No response body on success) + +#### Business Rules + +- Only deletes measure-owned options +- Does not affect catalog options +- After deletion, measure inherits from catalog (if linked) + +--- + +### 12. Copy Options from Catalog (Issue #469) + +Copy options from a catalog entry to create measure-owned options. Use this to "detach" a measure from its catalog's options for customization. + +**Endpoint:** `POST /measures/{id}/options/copy-from-catalog/{catalogId}` + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `id` | string (GUID) | **Yes** | Measure identifier | +| `catalogId` | string (GUID) | **Yes** | Catalog identifier to copy from | + +#### Response + +**Status:** `200 OK` + +```json +{ + "success": true, + "data": { + "options": [ + { + "id": "new-opt-001", + "measureId": "measure-123", + "catalogId": null, + "numericValue": 1, + "label": "Not Met", + "description": "Performance below target", + "sortOrder": 1 + }, + ... + ], + "isInherited": false, + "sourceCatalogId": null + }, + "error": null +} +``` + +#### Business Rules + +- Creates new option records owned by the measure +- Copied options are independent of catalog (future catalog changes don't affect them) +- If measure already has options, they are replaced + +--- + +## Data Models + +### MeasureDirection Enum + +```typescript +enum MeasureDirection { + Increase = "Increase", // Higher is better (e.g., revenue, customers) + Decrease = "Decrease" // Lower is better (e.g., costs, churn) +} +``` + +### MeasureType Enum + +> **Note:** MeasureType describes how values are **represented and input** (Quantitative = direct number, Qualitative = select from options). This is different from indicator classification (leading/lagging) which may be tracked separately. + +```typescript +enum MeasureType { + Quantitative = "Quantitative", // Direct numeric input (e.g., $50,000 revenue) + Qualitative = "Qualitative", // Select from labeled options (e.g., "Excellent" = 5) + Binary = "Binary" // DEPRECATED: Use Qualitative with 2 options instead +} +``` + +**Examples:** +- **Quantitative:** Revenue ($50,000), Temperature (72°F), Customer Count (1,500) +- **Qualitative:** Satisfaction (Excellent/Good/Fair/Poor → 4/3/2/1), Risk Level (High/Medium/Low → 3/2/1) +- **Binary (deprecated):** Complete (Yes/No) - migrate to Qualitative with options Yes=1, No=0 + + +### Common Categories + +Standard Measure categories (not enforced, but commonly used): +- `Finance` - Revenue, costs, profitability +- `Sales` - Pipeline, conversions, deals +- `Marketing` - Leads, CAC, ROAS +- `Customer Success` - Retention, satisfaction, NPS +- `Operations` - Efficiency, productivity, quality +- `Product` - Usage, adoption, feature metrics +- `HR` - Headcount, turnover, engagement + +--- + +## Error Handling + +### Standard Error Response + +```json +{ + "success": false, + "data": null, + "error": "Error message here" +} +``` + +### Common Error Codes + +| Code | Scenario | Message Example | +|------|----------|-----------------| +| 400 | Invalid GUID format | "Invalid Measure ID format" | +| 400 | Missing required field | "Name is required" | +| 400 | Invalid enum value | "Direction must be 'Increase' or 'Decrease'" | +| 401 | Missing/invalid token | "Unauthorized" | +| 403 | Insufficient permissions | "Access denied to this Measure" | +| 404 | Measure not found | "Measure not found" | +| 422 | Validation failure | "Target value must be greater than 0" | +| 500 | Server error | "Internal server error" | + +--- + +## Frontend Usage Examples + +### TypeScript Service + +```typescript +import { traction } from './traction'; + +// List Measures for a goal +const measures = await traction.get('/measures', { + params: { goalId: 'goal-123' } +}); + +// Create catalog-based Measure +const newMeasure = await traction.post('/measures', { + name: 'Customer Acquisition Cost', + catalogId: 'measure-catalog-003', + targetValue: 100.00, + unit: 'USD', + ownerId: 'owner-123', + strategyId: 'strategy-456' +}); + +// Update Measure value +await traction.put(`/measures/${measureId}/value`, { + value: 95.50, + recordedAt: new Date().toISOString(), + notes: 'Monthly update' +}); + +// Get Measure details with linked goals +const measureDetails = await traction.get(`/measures/${measureId}`); +console.log(`Linked to ${measureDetails.data.linkedGoals.length} goals`); + +// Delete Measure (soft delete) +await traction.delete(`/measures/${measureId}`); +``` + +--- + +## Related APIs + +- **[Measure Links API](./measure-links-api.md)** - Link Measures to goals +- **[Measure Data API](./measure-data-api.md)** - Record targets, actuals, projections +- **[Goals API](./goals-api.md)** - Manage goals that Measures measure +- **[Strategies API](./strategies-api.md)** - Strategies that Measures support + +--- + +## Changelog + +### v7.5 (February 6, 2026) - Issue #640: Measure Progress Calculation +- ✨ **Added:** `progress` field to GET /measures/{id} response +- 📊 Progress calculation at measure level includes only `progressPercent` (0-100+) +- 🔧 Progress calculated using primary link (or first link if no primary) thresholds +- ⚠️ **Note:** Status and variance are NOT included at measure level - those require thresholds which exist at the link level +- 💡 Calculated from baseline (first actual) to target using domain service for uniform calculation +- 🔄 Returns `null` if no links exist or insufficient data (requires 2+ actuals and 1+ target) + +### v7.4 (January 10, 2026) - Issue #527: Link-Level Progress +- ✨ **Added:** `progress` field to `goalLinks` and `strategyLinks` in GET /measures/summary response +- 🔄 **Changed:** Progress moved from measure level to link level for accurate per-goal/strategy tracking +- 📊 Progress calculation includes: progressPercentage, status (on_track/at_risk/behind/no_data), variance, variancePercentage, daysUntilTarget, isOverdue +- 🧮 Implemented via domain service pattern (`MeasureLinkProgressService`) following DDD principles +- 📝 Each link has independent progress based on its own threshold configuration + +### v7.3 (January 9, 2026) - Issue #526: Measure Summary Endpoint +- ✨ Added `GET /measures/summary` - Comprehensive measure tracker with filtering and aggregations +- 📊 Includes progress calculations, goal/strategy links, trend data, and summary statistics +- 🔍 Supports filtering by category, owner, status, progress status, and period +- 📈 Configurable trend data points (default: 5, supports 0 for all) +- 📝 Documented progress status calculation algorithm + +### v7.2 (January 8, 2026) - Issue #469: Streamline Measure Terminology +- ✨ Added `GET /measures/{id}/options` - Get options for Qualitative measures +- ✨ Added `PUT /measures/{id}/options` - Set/replace measure options +- ✨ Added `DELETE /measures/{id}/options` - Delete measure-owned options +- ✨ Added `POST /measures/{id}/options/copy-from-catalog/{catalogId}` - Copy catalog options +- 📝 **Fixed MeasureType enum documentation** - Changed from Leading/Lagging to Quantitative/Qualitative/Binary +- ⚠️ Deprecated `Binary` MeasureType - Use Qualitative with 2 options instead +- 📝 Clarified Type vs ValueType terminology (Type = input method, ValueType = data nature) + +### v7.1 (January 2, 2026) +- ✨ Added `GET /goals/available-measures` for new (unpersisted) goals +- 📝 Renamed spec files to `measures-*` for consistency + +### v7.0 (December 23, 2025) +- ✅ Documented all 7 endpoints with complete examples +- ⚠️ Deprecated `GET /measures/{id}/linked-goals` (returns 501) +- ⚠️ Deprecated `goalId` field in `POST /measures` request +- ✨ Added `linkedGoals` to `GET /measures/{id}` response +- ✨ Added `historicalValues` to detail response +- 📝 Documented catalog-based vs custom Measure creation +- 📝 Clarified filtering behavior and priorities +- 📝 Added frontend TypeScript usage examples + +### v6.0 (December 21, 2025) +- Initial Measure endpoints + +--- + +**[← Back to Traction Service Index](./README.md)** diff --git a/docs/shared/Specifications/user-app/traction-service/strategies-api.md b/docs/shared/Specifications/user-app/traction-service/strategies-api.md new file mode 100644 index 00000000..a7944525 --- /dev/null +++ b/docs/shared/Specifications/user-app/traction-service/strategies-api.md @@ -0,0 +1,395 @@ +# Strategies API Specification + +**Controller:** `StrategiesController` +**Base Route:** `/goals/{goalId}/strategies` +**Version:** 7.0 +**Last Updated:** December 26, 2025 + +[← Back to API Index](./README.md) + +--- + +## Overview + +The Strategies API manages execution strategies for business goals, including creation, updates, status transitions, reordering, and alignment scoring. + +**Endpoints:** 6 total +- 3 CRUD operations (List, Create, Update, Delete) +- 1 reorder endpoint +- 1 alignment update endpoint + +--- + +## Authentication + +All endpoints require: +- `Authorization: Bearer {accessToken}` +- `X-Tenant-Id: {tenantId}` + +--- + +## Endpoints + +### 1. List Strategies for Goal + +**GET** `/goals/{goalId}/strategies` + +Retrieve all strategies associated with a specific goal. + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `goalId` | string (GUID) | Yes | Goal identifier | + +#### Response (200 OK) + +```json +{ + "success": true, + "data": [ + { + "id": "strat-001", + "goalId": "goal-123", + "title": "Expand Product Line", + "description": "Launch 3 new products targeting SMB segment", + "status": "in_progress", + "priority": "high", + "order": 1, + "aiGenerated": false, + "validationScore": 85, + "validationFeedback": "Strong strategy with clear metrics", + "alignmentScore": 92, + "alignmentExplanation": "Aligns well with company vision and core values", + "alignmentSuggestions": [ + "Consider adding customer satisfaction metrics", + "Link to brand awareness Measures" + ], + "ownerId": "user-456", + "dueDate": "2025-12-31", + "tags": ["product", "growth"], + "createdAt": "2025-01-01T10:00:00Z", + "updatedAt": "2025-01-15T14:30:00Z" + } + ] +} +``` + +--- + +### 2. Create Strategy + +**POST** `/goals/{goalId}/strategies` + +Create a new strategy for a goal. + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `goalId` | string (GUID) | Yes | Goal identifier | + +#### Request Body + +```json +{ + "title": "Expand Product Line", + "description": "Launch 3 new products targeting SMB segment", + "ownerId": "user-456", + "priority": "high", + "dueDate": "2025-12-31", + "order": 1, + "aiGenerated": false +} +``` + +#### Response (201 Created) + +```json +{ + "success": true, + "data": { + "id": "strat-001", + "goalId": "goal-123", + "title": "Expand Product Line", + "description": "Launch 3 new products targeting SMB segment", + "status": "planning", + "priority": "high", + "order": 1, + "aiGenerated": false, + "ownerId": "user-456", + "dueDate": "2025-12-31", + "tags": [], + "createdAt": "2025-01-01T10:00:00Z" + } +} +``` + +--- + +### 3. Update Strategy + +**PUT** `/goals/{goalId}/strategies/{id}` + +Update an existing strategy. + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `goalId` | string (GUID) | Yes | Goal identifier | +| `id` | string (GUID) | Yes | Strategy identifier | + +#### Request Body + +All fields optional: + +```json +{ + "title": "Expand Product Line (Updated)", + "description": "Launch 5 new products targeting SMB and enterprise segments", + "status": "in_progress", + "ownerId": "user-789", + "priority": "critical", + "dueDate": "2025-11-30", + "order": 2, + "validationScore": 90, + "validationFeedback": "Excellent strategy with clear success criteria" +} +``` + +#### Response (200 OK) + +```json +{ + "success": true, + "data": { + "id": "strat-001", + "goalId": "goal-123", + "title": "Expand Product Line (Updated)", + "description": "Launch 5 new products targeting SMB and enterprise segments", + "status": "in_progress", + "priority": "critical", + "order": 2, + "validationScore": 90, + "validationFeedback": "Excellent strategy with clear success criteria", + "ownerId": "user-789", + "dueDate": "2025-11-30", + "updatedAt": "2025-01-20T16:45:00Z" + } +} +``` + +--- + +### 4. Update Strategy Alignment + +**PATCH** `/goals/{goalId}/strategies/{id}/alignment` + +Update strategy alignment scores calculated by the Coaching Service AI. + +**Purpose:** This endpoint persists alignment data calculated by the Coaching Service, which evaluates how well the strategy aligns with: +- The goal's intent and objectives +- The business foundation (vision, purpose, core values) +- Company strategy and market positioning + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `goalId` | string (GUID) | Yes | Goal identifier | +| `id` | string (GUID) | Yes | Strategy identifier | + +#### Request Body + +```json +{ + "alignmentScore": 92, + "alignmentExplanation": "Strategy aligns strongly with company vision to become market leader in cloud solutions. Direct connection to 'Innovation' and 'Customer Focus' core values. Supports goal of increasing market share by 15%.", + "alignmentSuggestions": [ + "Consider adding customer satisfaction metrics to track alignment with 'Customer Focus' value", + "Link to brand awareness Measures to measure market leadership progress" + ] +} +``` + +#### Request Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `alignmentScore` | number | Yes | 0-100: How well strategy aligns with goal and business foundation | +| `alignmentExplanation` | string | No | AI-generated explanation of the alignment score | +| `alignmentSuggestions` | string[] | No | AI-generated suggestions for improving alignment | + +#### Response (200 OK) + +```json +{ + "success": true, + "data": { + "id": "strat-001", + "goalId": "goal-123", + "title": "Expand Product Line", + "alignmentScore": 92, + "alignmentExplanation": "Strategy aligns strongly with company vision...", + "alignmentSuggestions": [ + "Consider adding customer satisfaction metrics...", + "Link to brand awareness Measures..." + ], + "updatedAt": "2025-01-21T10:15:00Z" + } +} +``` + +#### Business Rules + +- **Alignment Score:** Must be between 0-100 +- **Frontend Flow:** + 1. Frontend calls Coaching Service to calculate alignment + 2. Coaching Service returns alignment data + 3. Frontend calls this endpoint to persist alignment in Traction Service + 4. Strategy alignment is displayed in UI and influences recommendations + +--- + +### 5. Delete Strategy + +**DELETE** `/goals/{goalId}/strategies/{id}` + +Delete a strategy. + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `goalId` | string (GUID) | Yes | Goal identifier | +| `id` | string (GUID) | Yes | Strategy identifier | + +#### Response (200 OK) + +```json +{ + "success": true, + "data": { + "message": "Strategy deleted successfully" + } +} +``` + +--- + +### 6. Reorder Strategies + +**PUT** `/goals/{goalId}/strategies:reorder` + +Atomically update the display order of multiple strategies within a goal. + +#### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `goalId` | string (GUID) | Yes | Goal identifier | + +#### Request Body + +```json +{ + "strategyOrders": [ + { "strategyId": "strat-002", "newOrder": 1 }, + { "strategyId": "strat-001", "newOrder": 2 }, + { "strategyId": "strat-003", "newOrder": 3 } + ] +} +``` + +#### Response (200 OK) + +```json +{ + "success": true, + "data": true +} +``` + +--- + +## Data Models + +### Strategy Status + +| Value | Description | +|-------|-------------| +| `planning` | Strategy is being planned | +| `in_progress` | Strategy execution in progress | +| `completed` | Strategy successfully completed | +| `paused` | Strategy execution paused | +| `cancelled` | Strategy cancelled/abandoned | + +### Priority + +| Value | Description | +|-------|-------------| +| `low` | Low priority | +| `medium` | Medium priority | +| `high` | High priority | +| `critical` | Critical priority | + +--- + +## Error Responses + +### 400 Bad Request + +```json +{ + "success": false, + "error": "Invalid strategy ID format" +} +``` + +### 404 Not Found + +```json +{ + "success": false, + "error": "Strategy not found" +} +``` + +### 500 Internal Server Error + +```json +{ + "success": false, + "error": "Internal server error" +} +``` + +--- + +## Business Rules + +- **Strategy Order:** Must be unique within a goal +- **Alignment Score:** Optional until Coaching Service calculates it +- **AI Generated:** Flag indicates if strategy was suggested by AI +- **Validation Score:** Optional AI-generated quality score (0-100) +- **Delete:** Deleting a strategy removes it permanently + +--- + +## Related APIs + +- [Goals API](./goals-api.md) - Parent goals management +- [Measure Links API](./measure-links-api.md) - Link Measures to strategies + +--- + +## Version History + +### 7.0 (December 26, 2025) +- Added **PATCH /goals/{goalId}/strategies/{id}/alignment** endpoint +- Added alignment fields to strategy response: `alignmentScore`, `alignmentExplanation`, `alignmentSuggestions` +- Documented complete strategy lifecycle endpoints + +### 1.0 (Initial) +- Basic CRUD operations for strategies From 9404d09d5251ce1e3a8b78502ef4169b18d42984 Mon Sep 17 00:00:00 2001 From: Motty Chen Date: Wed, 4 Mar 2026 15:47:43 -0600 Subject: [PATCH 3/7] fix(cors): restore preprod and production preflight handling Ensure browser preflight requests always return CORS headers by configuring API Gateway for allowed frontend origins and relaxing app-level allowed headers. Add a stronger production smoke test that validates all purposepath.app frontend origins and common tracing headers to prevent regressions. Made-with: Cursor --- .github/workflows/deploy-production.yml | 51 +++++++++++++++---------- coaching/pulumi/__main__.py | 43 +++++++++++++++++++-- coaching/src/api/main.py | 14 ++----- 3 files changed, 72 insertions(+), 36 deletions(-) diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index dd2e5fca..a7b3e66d 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -277,28 +277,37 @@ jobs: - name: CORS Preflight Check run: | echo "Testing CORS preflight behavior..." - ORIGIN="https://purposepath.app" TARGET="${{ steps.api-url.outputs.url }}/api/v1/health" - - CORS_HEADERS=$(curl -s -D - -o /dev/null -X OPTIONS "$TARGET" \ - -H "Origin: $ORIGIN" \ - -H "Access-Control-Request-Method: GET" \ - -H "Access-Control-Request-Headers: Authorization,Content-Type") - - ALLOW_ORIGIN=$(echo "$CORS_HEADERS" | tr -d '\r' | awk -F': ' 'tolower($1)=="access-control-allow-origin"{print $2}' | tail -n 1) - ALLOW_CREDENTIALS=$(echo "$CORS_HEADERS" | tr -d '\r' | awk -F': ' 'tolower($1)=="access-control-allow-credentials"{print $2}' | tail -n 1) - - if [ "$ALLOW_ORIGIN" != "$ORIGIN" ]; then - echo "❌ Invalid Access-Control-Allow-Origin: '$ALLOW_ORIGIN' (expected '$ORIGIN')" - exit 1 - fi - - if [ "$ALLOW_CREDENTIALS" != "true" ]; then - echo "❌ Invalid Access-Control-Allow-Credentials: '$ALLOW_CREDENTIALS' (expected 'true')" - exit 1 - fi - - echo "✅ CORS preflight returned expected headers" + ORIGINS=( + "https://dev.purposepath.app" + "https://staging.purposepath.app" + "https://preprod.purposepath.app" + "https://www.purposepath.app" + "https://purposepath.app" + ) + + for ORIGIN in "${ORIGINS[@]}"; do + echo "Checking origin: $ORIGIN" + CORS_HEADERS=$(curl -s -D - -o /dev/null -X OPTIONS "$TARGET" \ + -H "Origin: $ORIGIN" \ + -H "Access-Control-Request-Method: GET" \ + -H "Access-Control-Request-Headers: Authorization,Content-Type,Baggage,Sentry-Trace") + + ALLOW_ORIGIN=$(echo "$CORS_HEADERS" | tr -d '\r' | awk -F': ' 'tolower($1)=="access-control-allow-origin"{print $2}' | tail -n 1) + ALLOW_CREDENTIALS=$(echo "$CORS_HEADERS" | tr -d '\r' | awk -F': ' 'tolower($1)=="access-control-allow-credentials"{print $2}' | tail -n 1) + + if [ "$ALLOW_ORIGIN" != "$ORIGIN" ]; then + echo "❌ Invalid Access-Control-Allow-Origin: '$ALLOW_ORIGIN' (expected '$ORIGIN')" + exit 1 + fi + + if [ "$ALLOW_CREDENTIALS" != "true" ]; then + echo "❌ Invalid Access-Control-Allow-Credentials: '$ALLOW_CREDENTIALS' (expected 'true')" + exit 1 + fi + done + + echo "✅ CORS preflight returned expected headers for all production frontend origins" - name: Smoke Test Summary run: | diff --git a/coaching/pulumi/__main__.py b/coaching/pulumi/__main__.py index 4d8be5c5..ea6ebb84 100644 --- a/coaching/pulumi/__main__.py +++ b/coaching/pulumi/__main__.py @@ -41,6 +41,20 @@ "business_api_base_url": "https://api.staging.purposepath.app/account/api/v1", "log_level": "INFO", }, + "preprod": { + "infra_stack": "mottych/purposepath-infrastructure/preprod", + "coaching_infra_stack": "mottych/purposepath-coaching-infrastructure/preprod", + "api_domain": "api.preprod.purposepath.app", + "certificate_output": "apiPreprod", + "jwt_secret": "purposepath-jwt-secret-preprod", + "openai_api_key_secret": "purposepath/preprod/openai-api-key", + "google_vertex_credentials_secret": "purposepath/preprod/google-vertex-credentials", + "jwt_issuer": "https://api.preprod.purposepath.app", + "jwt_audience": "https://preprod.purposepath.app", + "account_api_url": "https://api.preprod.purposepath.app", + "business_api_base_url": "https://api.preprod.purposepath.app/account/api/v1", + "log_level": "INFO", + }, "prod": { "infra_stack": "mottych/purposepath-infrastructure/prod", "coaching_infra_stack": "mottych/purposepath-coaching-infrastructure/prod", @@ -74,6 +88,10 @@ "basic": "CLAUDE_3_5_SONNET_V2", "premium": "CLAUDE_OPUS_4_5", }, + "preprod": { + "basic": "CLAUDE_3_5_SONNET_V2", + "premium": "CLAUDE_OPUS_4_5", + }, "prod": { "basic": "CLAUDE_3_5_SONNET_V2", "premium": "CLAUDE_OPUS_4_5", @@ -363,13 +381,30 @@ ) # API Gateway HTTP API -# IMPORTANT: CORS is intentionally handled only in FastAPI middleware. -# Keeping API Gateway CORS enabled created split-brain behavior where -# preflight responses could come from APIGW ("*") while app responses came -# from FastAPI (credential-aware origin regex), causing intermittent browser failures. +# CORS is configured at the gateway layer so browser preflight OPTIONS requests +# always receive CORS headers, even when requests are rejected before FastAPI. api = aws.apigatewayv2.Api( "coaching-api", protocol_type="HTTP", + cors_configuration={ + "allow_credentials": True, + "allow_headers": ["*"], + "allow_methods": ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], + "allow_origins": [ + "https://dev.purposepath.app", + "https://staging.purposepath.app", + "https://preprod.purposepath.app", + "https://www.purposepath.app", + "https://purposepath.app", + ], + "expose_headers": [ + "X-Request-Id", + "X-RateLimit-Limit", + "X-RateLimit-Remaining", + "X-RateLimit-Reset", + ], + "max_age": 3600, + }, ) integration = aws.apigatewayv2.Integration( diff --git a/coaching/src/api/main.py b/coaching/src/api/main.py index e4965fca..c2245c81 100644 --- a/coaching/src/api/main.py +++ b/coaching/src/api/main.py @@ -118,17 +118,9 @@ async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]: "allow_origin_regex": r"(^https://([a-zA-Z0-9-]+\.)*(purposepath|purpopsepath)\.app$)|(^http://localhost:\d+$)", "allow_credentials": True, "allow_methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"], - "allow_headers": [ - "Content-Type", - "Authorization", - "X-Requested-With", - "Accept", - "Origin", - "X-Api-Key", - "X-Tenant-Id", - "X-User-Id", - "X-CSRF-Token", - ], + # Allow all request headers to prevent preflight breakage when frontend tooling + # adds non-static headers (for example tracing/monitoring headers). + "allow_headers": ["*"], "expose_headers": [ "X-Request-Id", "X-RateLimit-Limit", From 610342c74bf35a3a42a360c05846bf127782c9da Mon Sep 17 00:00:00 2001 From: Motty Chen Date: Wed, 4 Mar 2026 16:02:30 -0600 Subject: [PATCH 4/7] chore(pulumi): add preprod stack config Add the coaching Pulumi preprod stack configuration file with AWS region so future hotfix branches can deploy to preprod without manual stack config setup. Made-with: Cursor --- coaching/pulumi/Pulumi.preprod.yaml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 coaching/pulumi/Pulumi.preprod.yaml diff --git a/coaching/pulumi/Pulumi.preprod.yaml b/coaching/pulumi/Pulumi.preprod.yaml new file mode 100644 index 00000000..1a38cefb --- /dev/null +++ b/coaching/pulumi/Pulumi.preprod.yaml @@ -0,0 +1,2 @@ +config: + aws:region: us-east-1 From 4a0ce1830ebe6ba3c618abd98d3386043684340f Mon Sep 17 00:00:00 2001 From: Motty Chen Date: Wed, 4 Mar 2026 16:39:30 -0600 Subject: [PATCH 5/7] fix(deploy): harden hotfix promotion reliability Support production deploys from merged hotfix PRs, add an automatic hotfix-to-preprod deployment workflow, and fail fast when Lambda is not Active after deploy. Stop managing the shared ECR repository from the coaching service stack to prevent image churn breaking Lambda. Made-with: Cursor --- .github/workflows/deploy-preprod-hotfix.yml | 181 ++++++++++++++++++++ .github/workflows/deploy-production.yml | 29 +++- coaching/pulumi/__main__.py | 19 +- 3 files changed, 212 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/deploy-preprod-hotfix.yml diff --git a/.github/workflows/deploy-preprod-hotfix.yml b/.github/workflows/deploy-preprod-hotfix.yml new file mode 100644 index 00000000..1857f4f9 --- /dev/null +++ b/.github/workflows/deploy-preprod-hotfix.yml @@ -0,0 +1,181 @@ +name: Deploy Preprod Hotfix + +on: + push: + branches: + - "hotfix/**" + workflow_dispatch: + inputs: + branch: + description: "Branch to deploy (defaults to current ref)" + required: false + type: string + skip_tests: + description: "Skip tests before deployment" + required: false + default: "false" + type: choice + options: + - "true" + - "false" + +concurrency: + group: deploy-preprod-hotfix-${{ github.ref_name }} + cancel-in-progress: true + +jobs: + pre-deployment-checks: + name: Pre-Deployment Validation + runs-on: ubuntu-latest + if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.skip_tests != 'true' }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.branch || github.ref }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Set up uv + uses: astral-sh/setup-uv@v5 + with: + version: "latest" + enable-cache: true + + - name: Create virtual environment + run: uv venv .venv + + - name: Install dependencies + run: | + source .venv/bin/activate + uv pip install -r coaching/requirements.txt + uv pip install -r coaching/requirements-dev.txt + shell: bash + + - name: Run Ruff Linting + run: | + source .venv/bin/activate + python -m ruff check . --exclude=".venv,venv,__pycache__,.pytest_cache" + shell: bash + + - name: Run MyPy Type Checking + run: | + source .venv/bin/activate + python -m mypy coaching/src/ shared/ --config-file=pyproject.toml + shell: bash + + - name: Run Unit Tests + run: | + source .venv/bin/activate + python -m pytest coaching/tests/unit/ -v --cov=coaching/src --cov-fail-under=70 + shell: bash + env: + PYTHONPATH: coaching:shared:. + + deploy-coaching: + name: Deploy to Preprod + runs-on: ubuntu-latest + needs: [pre-deployment-checks] + if: ${{ always() && (needs.pre-deployment-checks.result == 'success' || (github.event_name == 'workflow_dispatch' && github.event.inputs.skip_tests == 'true')) }} + permissions: + id-token: write + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.branch || github.ref }} + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install Pulumi Python dependencies + working-directory: coaching/pulumi + run: pip install -r requirements.txt + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: us-east-1 + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + - name: Deploy Coaching Service + uses: pulumi/actions@v5 + with: + command: up + stack-name: preprod + work-dir: coaching/pulumi + env: + PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: us-east-1 + + smoke-tests: + name: Post-Deployment Smoke Tests + runs-on: ubuntu-latest + needs: [deploy-coaching] + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.branch || github.ref }} + + - name: Install Pulumi CLI + uses: pulumi/actions@v5 + with: + pulumi-version: "latest" + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: us-east-1 + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + - name: Get API Gateway URL + id: api-url + working-directory: coaching/pulumi + run: | + URL=$(pulumi stack output customDomainUrl --stack preprod) + echo "url=$URL" >> $GITHUB_OUTPUT + env: + PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} + + - name: Health Check + run: | + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "${{ steps.api-url.outputs.url }}/health" || echo "000") + if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "404" ]; then + echo "❌ Health check failed with HTTP $HTTP_CODE" + exit 1 + fi + echo "✅ Health check passed ($HTTP_CODE)" + + - name: CORS Preflight Check + run: | + ORIGIN="https://preprod.purposepath.app" + TARGET="${{ steps.api-url.outputs.url }}/api/v1/ai/execute-async" + CORS_HEADERS=$(curl -s -D - -o /dev/null -X OPTIONS "$TARGET" \ + -H "Origin: $ORIGIN" \ + -H "Access-Control-Request-Method: POST" \ + -H "Access-Control-Request-Headers: Authorization,Content-Type,X-Tenant-Id") + + ALLOW_ORIGIN=$(echo "$CORS_HEADERS" | tr -d '\r' | awk -F': ' 'tolower($1)=="access-control-allow-origin"{print $2}' | tail -n 1) + ALLOW_CREDENTIALS=$(echo "$CORS_HEADERS" | tr -d '\r' | awk -F': ' 'tolower($1)=="access-control-allow-credentials"{print $2}' | tail -n 1) + + if [ "$ALLOW_ORIGIN" != "$ORIGIN" ]; then + echo "❌ Invalid Access-Control-Allow-Origin: '$ALLOW_ORIGIN' (expected '$ORIGIN')" + exit 1 + fi + if [ "$ALLOW_CREDENTIALS" != "true" ]; then + echo "❌ Invalid Access-Control-Allow-Credentials: '$ALLOW_CREDENTIALS' (expected 'true')" + exit 1 + fi + + echo "✅ CORS preflight returned expected headers" diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index a7b3e66d..616c653c 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -34,13 +34,20 @@ jobs: run: | SHOULD_DEPLOY="false" TRIGGER_REASON="not-eligible" + HEAD_REF="${{ github.event.pull_request.head.ref }}" if [ "${{ github.event_name }}" == "pull_request" ] && \ [ "${{ github.event.pull_request.merged }}" == "true" ] && \ [ "${{ github.event.pull_request.base.ref }}" == "master" ] && \ - [ "${{ github.event.pull_request.head.ref }}" == "staging" ]; then + [ "$HEAD_REF" == "staging" ]; then SHOULD_DEPLOY="true" TRIGGER_REASON="merged-staging-to-master-pr" + elif [ "${{ github.event_name }}" == "pull_request" ] && \ + [ "${{ github.event.pull_request.merged }}" == "true" ] && \ + [ "${{ github.event.pull_request.base.ref }}" == "master" ] && \ + [[ "$HEAD_REF" == hotfix/* ]]; then + SHOULD_DEPLOY="true" + TRIGGER_REASON="merged-hotfix-to-master-pr" elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then SHOULD_DEPLOY="true" TRIGGER_REASON="manual-dispatch" @@ -56,7 +63,7 @@ jobs: if: steps.promotion-check.outputs.should_deploy != 'true' run: | echo "No production deployment triggered." - echo "This workflow only deploys on merged PRs from staging -> master, or manual dispatch." + echo "This workflow deploys on merged PRs from staging/hotfix -> master, or manual dispatch." pre-deployment-checks: name: Pre-Deployment Validation @@ -199,6 +206,24 @@ jobs: env: PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} + - name: Lambda Runtime State Check + run: | + echo "Validating Lambda runtime state..." + LAMBDA_ARN=$(pulumi stack output lambdaArn --stack prod) + LAMBDA_NAME=${LAMBDA_ARN##*:function:} + + STATE=$(aws lambda get-function --function-name "$LAMBDA_NAME" --region us-east-1 --query "Configuration.State" --output text) + REASON=$(aws lambda get-function --function-name "$LAMBDA_NAME" --region us-east-1 --query "Configuration.StateReason" --output text) + + if [ "$STATE" != "Active" ]; then + echo "❌ Lambda is not Active (state=$STATE, reason=$REASON)" + exit 1 + fi + + echo "✅ Lambda state is Active" + env: + PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} + - name: Create GitHub Release uses: actions/create-release@v1 env: diff --git a/coaching/pulumi/__main__.py b/coaching/pulumi/__main__.py index ea6ebb84..51dd22fd 100644 --- a/coaching/pulumi/__main__.py +++ b/coaching/pulumi/__main__.py @@ -309,21 +309,10 @@ ), ) -# Reuse shared ECR repository when it already exists. -# This avoids cross-stack repository creation conflicts in production. -try: - existing_ecr_repo = aws.ecr.get_repository(name="purposepath-coaching") - ecr_repository_url = pulumi.Output.from_input(existing_ecr_repo.repository_url) -except Exception: - ecr_repo = aws.ecr.Repository( - "coaching-repo", - name="purposepath-coaching", - image_scanning_configuration=aws.ecr.RepositoryImageScanningConfigurationArgs( - scan_on_push=True - ), - force_delete=True, - ) - ecr_repository_url = ecr_repo.repository_url +# Use the shared ECR repository managed outside this stack. +# Do not create/delete this repository from service deployments. +existing_ecr_repo = aws.ecr.get_repository(name="purposepath-coaching") +ecr_repository_url = pulumi.Output.from_input(existing_ecr_repo.repository_url) # Build and push Docker image auth_token = aws.ecr.get_authorization_token() From 9c0435e02d71b0cb13ec9fd3a78f1b375cc6a1b2 Mon Sep 17 00:00:00 2001 From: Motty Chen Date: Wed, 4 Mar 2026 16:52:10 -0600 Subject: [PATCH 6/7] fix(ci): make production lambda check robust Run Lambda state validation from the Pulumi project directory and poll until the function reaches Active with a successful update status. This prevents false failures and correctly blocks only unhealthy production deploys. Made-with: Cursor --- .github/workflows/deploy-production.yml | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 616c653c..4c5e6e36 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -207,20 +207,29 @@ jobs: PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} - name: Lambda Runtime State Check + working-directory: coaching/pulumi run: | echo "Validating Lambda runtime state..." LAMBDA_ARN=$(pulumi stack output lambdaArn --stack prod) LAMBDA_NAME=${LAMBDA_ARN##*:function:} - STATE=$(aws lambda get-function --function-name "$LAMBDA_NAME" --region us-east-1 --query "Configuration.State" --output text) - REASON=$(aws lambda get-function --function-name "$LAMBDA_NAME" --region us-east-1 --query "Configuration.StateReason" --output text) + for ATTEMPT in {1..18}; do + STATE=$(aws lambda get-function --function-name "$LAMBDA_NAME" --region us-east-1 --query "Configuration.State" --output text) + REASON=$(aws lambda get-function --function-name "$LAMBDA_NAME" --region us-east-1 --query "Configuration.StateReason" --output text) + UPDATE_STATUS=$(aws lambda get-function --function-name "$LAMBDA_NAME" --region us-east-1 --query "Configuration.LastUpdateStatus" --output text) - if [ "$STATE" != "Active" ]; then - echo "❌ Lambda is not Active (state=$STATE, reason=$REASON)" - exit 1 - fi + echo "Attempt $ATTEMPT: state=$STATE, updateStatus=$UPDATE_STATUS, reason=$REASON" + + if [ "$STATE" == "Active" ] && [ "$UPDATE_STATUS" == "Successful" ]; then + echo "✅ Lambda state is Active and update status is Successful" + exit 0 + fi + + sleep 10 + done - echo "✅ Lambda state is Active" + echo "❌ Lambda did not reach Active/Successful in expected time window" + exit 1 env: PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} From a6b50d150172efea842f9cb6b6303768832aa921 Mon Sep 17 00:00:00 2001 From: Motty Chen Date: Wed, 4 Mar 2026 17:41:13 -0600 Subject: [PATCH 7/7] docs: add resolve-production-issue command workflow Add a dedicated Cursor command for production incident handling that mirrors resolve-issue principles while enforcing the hotfix path: preprod validation, hotfix-to-master promotion, and mandatory downstream merges to staging and dev. Made-with: Cursor --- .cursor/commands/resolve-production-issue.md | 106 +++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 .cursor/commands/resolve-production-issue.md diff --git a/.cursor/commands/resolve-production-issue.md b/.cursor/commands/resolve-production-issue.md new file mode 100644 index 00000000..e16eb29b --- /dev/null +++ b/.cursor/commands/resolve-production-issue.md @@ -0,0 +1,106 @@ +# Resolve Production Issue - Mandatory Hotfix Workflow + +## CRITICAL: Follow #file:../rules/cursorrules.mdc EXACTLY + +### STEP 1: Prepare Hotfix Branch (MANDATORY) + +#### Branch Setup +- [ ] Ensure clean working tree (starting from local `dev`) +- [ ] Fetch latest remote references: `git fetch origin` +- [ ] Create hotfix branch from remote master: `git checkout -b hotfix/issue-{ISSUE}-{slug} origin/master` +- [ ] Add `in-progress` label to the issue: `gh issue edit {ISSUE} --add-label in-progress` +- [ ] Keep implementation scoped only to production fix requirements +- [ ] Ensure preprod compatibility for config/deployment updates + +--- + +### STEP 2: Issue Intake & Plan Confirmation + +- [ ] Read the full issue, incident details, and any linked docs/runbooks +- [ ] Identify blast radius (service, API routes, infra, data, auth, tenant impact) +- [ ] Define rollback and validation strategy before coding +- [ ] Post issue comment with: + - root-cause hypothesis + - planned fix scope + - preprod + production validation plan +- [ ] STOP! Announce plan and wait for user approval before changes + +### STEP 3: Implement Hotfix (Scoped + Safe) + +- [ ] Implement only the minimal safe fix for production behavior +- [ ] Follow Python + FastAPI clean architecture conventions: + - Domain entities in `coaching/src/domain/entities/` + - Value objects in `coaching/src/domain/value_objects/` + - Ports in `coaching/src/domain/ports/` + - Application services in `coaching/src/application/` + - Infrastructure adapters in `coaching/src/infrastructure/` + - API routes in `coaching/src/api/routes/` + - Core types/constants in `coaching/src/core/` +- [ ] Use Pydantic models, not untyped `dict[str, Any]` in domain +- [ ] Preserve tenant isolation checks on all data access +- [ ] Update workflow/deployment checks when reliability is part of the incident +- [ ] Remove temporary code, debug logs, and dead code + +### STEP 4: Validate on Hotfix Branch (Preprod First) + +- [ ] Run lint/type/tests locally before push: + ```powershell + # Lint + format + python -m ruff check coaching/ shared/ --fix + python -m ruff format coaching/ shared/ + + # Type checking + python -m mypy coaching/src shared/ --explicit-package-bases + + # Tests + cd coaching && uv run pytest --cov=src + ``` +- [ ] Push hotfix branch: `git push -u origin hotfix/issue-{ISSUE}-{slug}` +- [ ] Confirm preprod deployment workflow runs successfully +- [ ] Execute manual validation in preprod for the incident scenario +- [ ] If validation fails: fix on same hotfix branch and repeat this step + +### STEP 5: Promote Hotfix to Production + +- [ ] Open PR `hotfix/issue-{ISSUE}-{slug} -> master` +- [ ] Include incident context, root cause, and validation evidence in PR body +- [ ] Merge PR only after preprod validation is confirmed +- [ ] Watch production deployment workflow end-to-end +- [ ] Verify post-deploy checks: + - Lambda/runtime state healthy (if applicable) + - API health endpoint responsive + - CORS preflight/critical route behavior validated + +### STEP 6: Propagate Fix Downstream (MANDATORY) + +- [ ] Open PR `master -> staging`, merge after checks pass +- [ ] Open PR `staging -> dev`, merge after checks pass +- [ ] Ensure all three branches now contain the hotfix commit(s) + +### STEP 7: Close Incident + Cleanup + +- [ ] Post closing summary on the issue with: + - root cause + - final fix + - validation evidence (preprod + production) + - follow-up actions (if any) +- [ ] Remove `in-progress` label +- [ ] Close issue with state_reason: `completed` +- [ ] Delete hotfix branches (local + remote) only after downstream merges: + ```powershell + git branch -d hotfix/issue-{ISSUE}-{slug} + git push origin --delete hotfix/issue-{ISSUE}-{slug} + ``` + +--- + +**Non-negotiables:** +- ❌ Never commit directly to `dev`/`staging`/`master` +- ❌ Never skip preprod validation before `hotfix -> master` merge +- ❌ Never leave workflow reliability gaps unverified for deployment incidents +- ❌ Never leave mock data, TODOs, or `dict[str, Any]` in domain +- ❌ Never skip tenant isolation checks +- ✅ Always keep scope to production incident requirements +- ✅ Always document root cause and validation evidence +- ✅ Always merge `master -> staging -> dev` after hotfix production release +- ✅ Always keep docs/config/workflows in sync with the fix