# Teams Phone CLI Architecture Document

**Version:** 1.0
**Date:** 2026-01-28
**Status:** Draft

---

## 1. Introduction

This document outlines the overall project architecture for **Teams Phone CLI**, including backend systems, shared services, and non-UI specific concerns. Its primary goal is to serve as the guiding architectural blueprint for AI-driven development, ensuring consistency and adherence to chosen patterns and technologies.

**Relationship to Frontend Architecture:**
This is a CLI-only project with no graphical user interface. No separate Frontend Architecture Document is needed.

### 1.1 Starter Template or Existing Project

**Starter Template:** N/A - This is a greenfield Python CLI project.

The project will be built from scratch using standard Python project conventions. No starter template or boilerplate is being used. The tech stack (Python 3.10+, Typer, httpx, MSAL) is well-documented and doesn't require a starter template for rapid setup.

### 1.2 Change Log

| Date | Version | Description | Author |
|------|---------|-------------|--------|
| 2026-01-28 | 1.0 | Initial architecture document | Architect Agent |

---

## 2. High Level Architecture

### 2.1 Technical Summary

The Teams Phone CLI is a **layered command-line application** built with Python that interfaces with Microsoft Graph API (beta) to manage Teams Phone configurations. The architecture follows a classic three-tier design: CLI commands (Typer), business logic services, and infrastructure/data access components. It uses an **async-capable HTTP client** (httpx) with built-in retry/throttling logic to handle Graph API's rate limits and async operations. Local CSV caching supplements API gaps for emergency locations and policy listings. The design prioritizes **automation and scripting** over interactive use, with multi-tenant support as a first-class feature.

### 2.2 High Level Overview

Based on the PRD Technical Assumptions:

1. **Architectural Style:** Modular Monolith (single deployable CLI with clear internal boundaries)
2. **Repository Structure:** Monorepo (single repo containing CLI, tests, scripts, documentation)
3. **Service Architecture:** Single-process CLI with layered internal services
4. **Primary Data Flow:**
   ```
   User → CLI Commands → Services → Graph API / CSV Cache → Response → Formatted Output
   ```
5. **Key Architectural Decisions:**

| Decision | Choice | Rationale |
|----------|--------|-----------|
| Language | Python 3.10+ | Cross-platform, rich ecosystem, Typer/httpx support |
| CLI Framework | Typer | Modern, type-hinted, auto-completion, excellent DX |
| HTTP Client | httpx | Async support, timeout handling, connection pooling |
| Auth Library | MSAL | Official Microsoft library, handles token refresh |
| Data Validation | Pydantic v2 | Strict mode for contract validation, fast serialization |
| Output Formatting | Rich | Tables, progress bars, color-coded status |

### 2.3 High Level Project Diagram

```mermaid
graph TB
    subgraph "User Environment"
        User[IT Admin / MSP]
        Terminal[Terminal / Script]
    end

    subgraph "Teams Phone CLI"
        CLI[CLI Layer<br/>Typer Commands]

        subgraph "Services"
            UserSvc[UserService]
            NumSvc[NumberService]
            PolSvc[PolicyService]
            LocSvc[LocationService]
            TenantSvc[TenantService]
            AuthSvc[AuthService]
        end

        subgraph "Infrastructure"
            GraphClient[GraphClient<br/>Retry/Throttling]
            CacheMgr[CacheManager<br/>Tokens/Data]
            ConfigMgr[ConfigManager<br/>TOML/Tenants]
            Formatter[OutputFormatter<br/>Tables/JSON]
        end
    end

    subgraph "External Systems"
        GraphAPI[Microsoft Graph API<br/>Beta Endpoints]
        EntraID[Microsoft Entra ID<br/>Authentication]
    end

    subgraph "Local Storage"
        Config[~/.teams-phone/config.toml]
        TokenCache[Token Cache<br/>Encrypted]
        CSVCache[CSV Cache<br/>Locations/Policies]
    end

    User --> Terminal
    Terminal --> CLI
    CLI --> UserSvc & NumSvc & PolSvc & LocSvc & TenantSvc & AuthSvc

    UserSvc & NumSvc & PolSvc --> GraphClient
    LocSvc --> CSVCache
    AuthSvc --> EntraID

    GraphClient --> GraphAPI
    GraphClient --> CacheMgr
    TenantSvc --> ConfigMgr
    ConfigMgr --> Config
    CacheMgr --> TokenCache & CSVCache

    CLI --> Formatter
```

### 2.4 Architectural and Design Patterns

| Pattern | Description | Rationale |
|---------|-------------|-----------|
| **Layered Architecture** | CLI → Service → Infrastructure separation | Clear boundaries, testable services, PRD-specified structure |
| **Repository Pattern** | Services abstract data access (Graph API, CSV) | Enables mocking for tests, future API migration flexibility |
| **Service Layer** | Business logic encapsulated in domain services | Single responsibility, reusable across commands |
| **Async Operations with Polling** | Graph assign/unassign return 202; poll for completion | Required by Graph API design; provides user feedback |
| **Configuration as Code** | TOML-based tenant profiles with env var secrets | Cross-platform, human-readable, security best practice |
| **Strict Schema Validation** | Pydantic models with `extra='forbid'` | Fail-fast on API contract changes (beta API risk mitigation) |
| **Graceful Degradation** | CSV cache optional; warnings when stale/missing | CLI remains functional even without location/policy data |
| **Retry with Exponential Backoff** | 3 retries on 429/503 errors | Graph API throttling resilience per PRD requirements |

---

## 3. Tech Stack

This is the **DEFINITIVE** technology selection section. All choices documented here are the single source of truth for the project.

### 3.1 Cloud Infrastructure

- **Provider:** Microsoft Azure (via Microsoft Graph API)
- **Key Services:** Microsoft Graph API (Beta), Microsoft Entra ID (authentication)
- **Deployment Regions:** N/A - CLI runs locally; connects to Microsoft's global Graph API endpoints

### 3.2 Technology Stack Table

| Category | Technology | Version | Purpose | Rationale |
|----------|------------|---------|---------|-----------|
| **Language** | Python | 3.10+ | Primary development language | Cross-platform, rich ecosystem, type hints, async support |
| **CLI Framework** | Typer | ^0.9.0 | Command-line interface | Modern CLI framework, auto-completion, type-based args, Rich integration |
| **HTTP Client** | httpx | ^0.25.0 | Async HTTP requests to Graph API | Native async, timeout handling, connection pooling, modern API |
| **Authentication** | msal | ^1.24.0 | Microsoft Entra ID auth | Official Microsoft library, token caching, certificate support |
| **Data Validation** | Pydantic | ^2.0.0 | Schema validation for API responses | Strict mode for contract validation, fast serialization |
| **Terminal UI** | Rich | ^13.0.0 | Tables, progress bars, colored output | Beautiful terminal output, Typer integration, cross-platform |
| **Config Parsing** | tomli / tomllib | stdlib (3.11+) / ^2.0.0 | TOML configuration files | Human-readable config, stdlib in Python 3.11+ |
| **CSV Handling** | csv (stdlib) | stdlib | Read location/policy CSV cache | No external dependency needed |
| **Testing** | pytest | ^7.4.0 | Unit and integration testing | Industry standard, excellent fixtures, async support |
| **Test Mocking** | pytest-mock | ^3.12.0 | Mock Graph API responses | Clean mocking syntax, pytest integration |
| **Async Testing** | pytest-asyncio | ^0.21.0 | Test async code paths | Required for httpx async testing |
| **HTTP Mocking** | respx | ^0.20.0 | Mock httpx requests | Purpose-built for httpx, clean API |
| **Code Formatting** | Ruff | ^0.1.0 | Linting and formatting | Fast, replaces flake8/black/isort, single tool |
| **Type Checking** | mypy | ^1.7.0 | Static type analysis | Catch type errors before runtime |
| **Package Management** | uv | ^0.1.0 | Fast Python package installer | 10-100x faster than pip, lockfile support |

### 3.3 Development Tools

| Tool | Purpose | Notes |
|------|---------|-------|
| **uv** | Package management, virtual environments | Modern replacement for pip + venv |
| **Ruff** | Linting + formatting (single tool) | Replaces flake8, black, isort |
| **mypy** | Type checking | Strict mode recommended |
| **pre-commit** | Git hooks for quality checks | Run ruff, mypy before commits |

### 3.4 External Dependencies

| System | Requirement | Purpose |
|--------|-------------|---------|
| Microsoft Entra ID | App Registration with service principal | Graph API authentication |
| Microsoft Graph API | Beta endpoints access | Teams Phone management operations |
| PowerShell + MicrosoftTeams module | Optional (for CSV export) | Export locations/policies to CSV |

---

## 4. Data Models

### 4.1 UserConfiguration

**Purpose:** Represents a Teams user's voice configuration, including phone number, license status, and policy assignments.

| Attribute | Type | Description |
|-----------|------|-------------|
| `id` | UUID | User's Entra object ID |
| `user_principal_name` | str | User's UPN (email format) |
| `display_name` | str | User's display name |
| `account_type` | Enum | `user`, `resourceAccount`, `guest`, `sfbOnPremUser`, `unknown` |
| `is_enterprise_voice_enabled` | bool | Whether voice is enabled |
| `feature_types` | list[str] | Features like `Teams`, `CallingPlan`, `DirectRouting` |
| `telephone_numbers` | list[TelephoneNumber] | Assigned phone numbers |
| `effective_policy_assignments` | list[PolicyAssignment] | All effective policies with source |

**Relationships:**
- Has many `TelephoneNumber` (typically 0-1 primary)
- Has many `PolicyAssignment` (one per policy type)

### 4.2 NumberAssignment

**Purpose:** Represents a phone number in the tenant's inventory with its assignment status and capabilities.

| Attribute | Type | Description |
|-----------|------|-------------|
| `id` | str | Graph API resource ID |
| `telephone_number` | str | E.164 format (+12065551234) |
| `number_type` | Enum | `callingPlan`, `directRouting`, `operatorConnect` |
| `assignment_status` | Enum | `unassigned`, `userAssigned`, `conferenceAssigned`, `voiceApplicationAssigned` |
| `activation_state` | Enum | `activated`, `assignmentPending`, `assignmentFailed`, `updatePending`, `updateFailed` |
| `capabilities` | list[str] | `userAssignment`, `conferenceAssignment`, `voiceApplicationAssignment` |
| `assignment_target_id` | UUID | Assigned user's object ID (if assigned) |
| `assignment_category` | Enum | `primary`, `private`, `alternate` (if assigned) |
| `location_id` | UUID | Emergency location ID |
| `civic_address_id` | UUID | Parent civic address ID |
| `city` | str | City where number is provisioned |
| `iso_country_code` | str | Country code (e.g., "US") |

**Relationships:**
- Belongs to one `UserConfiguration` (when assigned)
- References one `EmergencyLocation` (via location_id)

### 4.3 PolicyAssignment

**Purpose:** Represents a policy assigned to a user, including the assignment source (direct, group, or global).

| Attribute | Type | Description |
|-----------|------|-------------|
| `policy_type` | str | Policy type (e.g., `TeamsCallingPolicy`, `TenantDialPlan`) |
| `policy_id` | str | Policy identifier |
| `display_name` | str | Human-readable policy name |
| `assignment_type` | Enum | `direct`, `group`, `global` |
| `group_id` | UUID | Group ID if assigned via group |

**Relationships:**
- Belongs to one `UserConfiguration`
- References one `Policy` (from CSV cache)

### 4.4 EmergencyLocation

**Purpose:** Represents an emergency location for E911 compliance (from PowerShell CSV export).

| Attribute | Type | Description |
|-----------|------|-------------|
| `location_id` | UUID | Unique location identifier (used in number assignment) |
| `civic_address_id` | UUID | Parent civic address ID |
| `description` | str | Location name (e.g., "Main Office - Floor 3") |
| `company_name` | str | Organization name |
| `address` | str | Full street address |
| `city` | str | City name |
| `state_or_province` | str | State/province code |
| `postal_code` | str | ZIP/postal code |
| `country_or_region` | str | Country code (e.g., "US") |
| `is_default` | bool | Whether this is the default location for the civic address |

**Relationships:**
- Referenced by `NumberAssignment` (via location_id)

### 4.5 TenantProfile

**Purpose:** Represents a configured tenant for multi-tenant CLI operations (stored in config.toml).

| Attribute | Type | Description |
|-----------|------|-------------|
| `name` | str | Profile name (e.g., "contoso") |
| `tenant_id` | UUID | Microsoft Entra tenant ID |
| `client_id` | UUID | App registration client ID |
| `display_name` | str | Human-readable tenant name |
| `auth_method` | Enum | `certificate`, `client-secret` |
| `cert_path` | str | Path to certificate file (if certificate auth) |
| `default_location` | str | Default emergency location name for this tenant |

**Relationships:**
- Has cached authentication tokens
- Has associated CSV cache files

### 4.6 AsyncOperation

**Purpose:** Represents a long-running Graph API operation (assign/unassign number).

| Attribute | Type | Description |
|-----------|------|-------------|
| `id` | str | Operation ID (base64 encoded) |
| `status` | Enum | `notStarted`, `running`, `succeeded`, `failed`, `skipped` |
| `created_datetime` | datetime | When operation was created |
| `numbers` | list[OperationNumber] | Status per phone number in operation |

**Relationships:**
- Created by `NumberService.assign()` and `NumberService.unassign()`

### 4.7 Policy (Reference Data)

**Purpose:** Represents a policy definition from CSV cache (for validation and display).

| Attribute | Type | Description |
|-----------|------|-------------|
| `policy_type` | str | Policy type identifier |
| `policy_name` | str | Policy name (Identity) |
| `policy_id` | str | Policy ID for API calls |
| `description` | str | Policy description |
| `is_global` | bool | Whether this is the global/default policy |

**Relationships:**
- Referenced by `PolicyAssignment`

---

## 5. Components

### 5.1 CLI Layer

#### Command Groups (Typer)

**Responsibility:** Parse command-line arguments, validate input, invoke services, format output.

**Key Interfaces:**
- `users` - User management commands (list, show, search)
- `numbers` - Phone number commands (list, show, search, assign, unassign, bulk-assign)
- `policies` - Policy commands (list, show, assign)
- `locations` - Location commands (list, show, search)
- `tenants` - Tenant profile commands (list, add, remove, switch, test)
- `auth` - Authentication commands (login, logout, status, refresh)
- `api-check` - Contract validation command

**Dependencies:** All Service layer components, OutputFormatter

**Technology Stack:** Typer, Rich (for output)

### 5.2 Service Layer

#### 5.2.1 UserService

**Responsibility:** Manage user voice configurations - list, search, and retrieve user details.

**Key Interfaces:**
- `list_users(filters, page_size, skip)` → list[UserConfiguration]
- `get_user(user_id)` → UserConfiguration
- `search_users(query)` → list[UserConfiguration]
- `resolve_user(identifier)` → UserConfiguration (by UPN, name, or ID)

**Dependencies:** GraphClient, CacheManager

#### 5.2.2 NumberService

**Responsibility:** Manage phone number inventory - list, search, assign, unassign, update.

**Key Interfaces:**
- `list_numbers(filters, page_size)` → list[NumberAssignment]
- `get_number(phone_number)` → NumberAssignment
- `search_numbers(pattern, status, type)` → list[NumberAssignment]
- `assign_number(user_id, phone_number, number_type, location_id)` → AsyncOperation
- `unassign_number(phone_number, number_type)` → AsyncOperation
- `update_number(phone_number, location_id, network_site_id)` → None
- `poll_operation(operation_id, timeout)` → AsyncOperation

**Dependencies:** GraphClient, LocationService (for validation), CacheManager

#### 5.2.3 PolicyService

**Responsibility:** Manage policy assignments - list available policies, show user policies, assign policies.

**Key Interfaces:**
- `list_policies(policy_type)` → list[Policy] (from CSV)
- `get_user_policies(user_id)` → list[PolicyAssignment] (via UserService)
- `assign_policy(user_id, policy_type, policy_id)` → None
- `validate_policy(policy_type, policy_name)` → bool

**Dependencies:** GraphClient, CacheManager (CSV reading)

#### 5.2.4 LocationService

**Responsibility:** Manage emergency locations from CSV cache - list, search, validate.

**Key Interfaces:**
- `list_locations()` → list[EmergencyLocation]
- `get_location(location_id)` → EmergencyLocation
- `search_locations(query)` → list[EmergencyLocation]
- `validate_location(location_id)` → bool
- `get_cache_age()` → timedelta
- `is_cache_stale()` → bool

**Dependencies:** CacheManager (CSV reading)

#### 5.2.5 TenantService

**Responsibility:** Manage tenant profiles - CRUD operations, switching active tenant.

**Key Interfaces:**
- `list_tenants()` → list[TenantProfile]
- `get_current_tenant()` → TenantProfile
- `switch_tenant(name)` → TenantProfile
- `add_tenant(profile)` → TenantProfile
- `remove_tenant(name)` → None
- `test_connection(name)` → ConnectionTestResult

**Dependencies:** ConfigManager, AuthService

#### 5.2.6 AuthService

**Responsibility:** Handle Microsoft Entra ID authentication - login, token management, refresh.

**Key Interfaces:**
- `login(tenant_profile, force)` → AuthToken
- `logout(tenant_name, all)` → None
- `get_token(tenant_name)` → str (access token)
- `get_status()` → AuthStatus
- `refresh_token(tenant_name)` → AuthToken
- `is_authenticated(tenant_name)` → bool

**Dependencies:** ConfigManager (for tenant config), CacheManager (token storage)

**Technology Stack:** MSAL, certificate/secret handling

### 5.3 Infrastructure Layer

#### 5.3.1 GraphClient

**Responsibility:** HTTP client for Microsoft Graph API with retry, throttling, and error handling.

**Key Interfaces:**
- `get(endpoint, params)` → dict
- `post(endpoint, body)` → dict | AsyncOperation
- `batch(requests)` → list[BatchResponse]
- `get_with_pagination(endpoint, params, max_items)` → AsyncIterator[dict]

**Dependencies:** AuthService (for tokens), CacheManager (response caching)

**Technology Stack:** httpx, exponential backoff, rate limit handling

**Key Behaviors:**
- Automatic retry on 429 (throttled) and 503 (service unavailable)
- Exponential backoff: 3 retries with 1s, 2s, 4s delays
- Request timeout: 30 seconds default
- Automatic token refresh on 401
- Beta API base URL: `https://graph.microsoft.com/beta`

#### 5.3.2 CacheManager

**Responsibility:** Manage all cached data - tokens, API responses, CSV files.

**Key Interfaces:**
- `get_token(tenant_name)` → CachedToken
- `save_token(tenant_name, token)` → None
- `clear_tokens(tenant_name)` → None
- `read_csv(filename)` → list[dict]
- `get_csv_metadata()` → CacheMetadata
- `get_cached_response(key)` → dict | None
- `save_cached_response(key, data, ttl)` → None

**Dependencies:** None (lowest level)

**Technology Stack:** File I/O, JSON, CSV, optional encryption for tokens

**Cache Locations:**
- Config: `~/.teams-phone/config.toml`
- Tokens: `~/.teams-phone/tokens/` (encrypted)
- CSV Cache: `~/.teams-phone/cache/`

#### 5.3.3 ConfigManager

**Responsibility:** Read/write TOML configuration file and tenant profiles.

**Key Interfaces:**
- `load_config()` → Config
- `save_config(config)` → None
- `get_tenant(name)` → TenantProfile
- `set_tenant(name, profile)` → None
- `get_default_tenant()` → str
- `set_default_tenant(name)` → None

**Dependencies:** None (lowest level)

**Technology Stack:** tomli/tomllib, Pydantic models

#### 5.3.4 OutputFormatter

**Responsibility:** Format command output - tables, JSON, progress bars, errors.

**Key Interfaces:**
- `table(data, columns, title)` → None (prints)
- `json(data)` → None (prints)
- `success(message)` → None
- `error(message, remediation)` → None
- `warning(message)` → None
- `progress_bar(total, description)` → ProgressContext

**Dependencies:** None

**Technology Stack:** Rich (tables, progress, colors)

### 5.4 Component Diagram

```mermaid
graph TB
    subgraph "CLI Layer"
        CMD[Typer Commands<br/>users/numbers/policies/locations/tenants/auth]
    end

    subgraph "Service Layer"
        US[UserService]
        NS[NumberService]
        PS[PolicyService]
        LS[LocationService]
        TS[TenantService]
        AS[AuthService]
    end

    subgraph "Infrastructure Layer"
        GC[GraphClient]
        CM[CacheManager]
        CFG[ConfigManager]
        OF[OutputFormatter]
    end

    CMD --> US & NS & PS & LS & TS & AS
    CMD --> OF

    US --> GC & CM
    NS --> GC & CM & LS
    PS --> GC & CM
    LS --> CM
    TS --> CFG & AS
    AS --> CFG & CM

    GC --> AS
    GC --> CM
```

---

## 6. External APIs

### 6.1 Microsoft Graph API (Teams Administration)

- **Purpose:** Primary API for all Teams Phone management operations
- **Documentation:** https://learn.microsoft.com/en-us/graph/api/resources/teamsadministration-teamsadminroot
- **Base URL:** `https://graph.microsoft.com/beta`
- **Authentication:** OAuth 2.0 Bearer token (via MSAL)
- **Rate Limits:** Throttling returns HTTP 429 with `Retry-After` header

**Key Endpoints:**

| Method | Endpoint | Purpose |
|--------|----------|---------|
| `GET` | `/admin/teams/userConfigurations` | List user voice configurations |
| `GET` | `/admin/teams/userConfigurations/{id}` | Get single user configuration |
| `GET` | `/admin/teams/telephoneNumberManagement/numberAssignments` | List phone number inventory |
| `GET` | `/admin/teams/telephoneNumberManagement/numberAssignments/{id}` | Get single number details |
| `POST` | `/admin/teams/telephoneNumberManagement/numberAssignments/assignNumber` | Assign number to user (async) |
| `POST` | `/admin/teams/telephoneNumberManagement/numberAssignments/unassignNumber` | Unassign number (async) |
| `POST` | `/admin/teams/telephoneNumberManagement/numberAssignments/updateNumber` | Update number location |
| `GET` | `/admin/teams/telephoneNumberManagement/operations/{id}` | Poll async operation status |
| `POST` | `/admin/teams/policy/userAssignments/assign` | Assign policy to user |
| `GET` | `/users` | Search/resolve users (v1.0) |
| `GET` | `/organization` | Get tenant info (v1.0) |
| `POST` | `/beta/$batch` | Batch multiple requests |

**Integration Notes:**
- All `/admin/teams/*` endpoints are **beta only** - no v1.0 equivalents
- Assign/unassign operations are **async** - return 202 with Location header for polling
- Phone number format varies: digits-only for assign/unassign, E.164 (+prefix) for update
- `locationId` is schema-optional but **backend-enforced for Calling Plan** numbers (E911)
- Batch endpoint must use `beta/$batch` for Teams Admin APIs

### 6.2 Microsoft Entra ID (Authentication)

- **Purpose:** OAuth 2.0 authentication for service principal
- **Documentation:** https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow
- **Base URL:** `https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0`
- **Authentication:** Certificate or client secret

### 6.3 Required API Permissions

| Permission | Type | Purpose |
|------------|------|---------|
| `TeamsUserConfiguration.Read.All` | Application | Read user voice configurations |
| `TeamsTelephoneNumber.Read.All` | Application | Read phone number inventory |
| `TeamsTelephoneNumber.ReadWrite.All` | Application | Assign/unassign/update numbers |
| `TeamsPolicyUserAssign.ReadWrite.All` | Application | Assign policies to users |
| `User.Read.All` | Application | Read user profiles, search users |
| `Directory.Read.All` | Application | Read tenant/organization info |

### 6.4 API Limitations (No Graph Endpoint Available)

| Operation | Workaround | Impact |
|-----------|------------|--------|
| List emergency locations | PowerShell `Get-CsOnlineLisLocation` → CSV | CLI reads from CSV cache |
| List policies by type | PowerShell `Get-CsTeamsCallingPolicy` etc. → CSV | CLI reads from CSV cache |
| Reserve/unreserve numbers | None exists | Feature dropped from MVP |

---

## 7. Core Workflows

### 7.1 Authentication Flow

```mermaid
sequenceDiagram
    participant User
    participant CLI
    participant AuthService
    participant ConfigManager
    participant CacheManager
    participant MSAL
    participant EntraID as Microsoft Entra ID

    User->>CLI: auth login --tenant contoso
    CLI->>ConfigManager: get_tenant("contoso")
    ConfigManager-->>CLI: TenantProfile
    CLI->>AuthService: login(tenant_profile)

    AuthService->>CacheManager: get_token("contoso")
    alt Token cached and valid
        CacheManager-->>AuthService: CachedToken
        AuthService-->>CLI: AuthToken (from cache)
    else Token missing or expired
        AuthService->>MSAL: acquire_token_for_client(scopes)
        MSAL->>EntraID: POST /token (certificate or secret)
        EntraID-->>MSAL: Access Token + Refresh Token
        MSAL-->>AuthService: TokenResponse
        AuthService->>CacheManager: save_token("contoso", token)
        AuthService-->>CLI: AuthToken (fresh)
    end

    CLI->>User: Authenticated to Contoso Ltd (expires in 59 min)
```

### 7.2 User Lookup Flow

```mermaid
sequenceDiagram
    participant User
    participant CLI
    participant UserService
    participant GraphClient
    participant AuthService
    participant GraphAPI as Microsoft Graph API

    User->>CLI: users show john@contoso.com
    CLI->>UserService: resolve_user("john@contoso.com")
    UserService->>GraphClient: get("/admin/teams/userConfigurations/{id}")

    GraphClient->>AuthService: get_token()
    AuthService-->>GraphClient: Bearer Token

    GraphClient->>GraphAPI: GET /admin/teams/userConfigurations/{id}
    GraphAPI-->>GraphClient: 200 OK + UserConfiguration JSON

    GraphClient-->>UserService: Response dict
    UserService-->>UserService: Validate with Pydantic model
    UserService-->>CLI: UserConfiguration

    CLI->>User: Display formatted table with phone, policies, status
```

### 7.3 Assign Phone Number Flow (Async)

```mermaid
sequenceDiagram
    participant User
    participant CLI
    participant NumberService
    participant LocationService
    participant GraphClient
    participant GraphAPI as Microsoft Graph API

    User->>CLI: numbers assign john@contoso.com +12065551234 --location "Seattle-HQ"

    CLI->>LocationService: validate_location("Seattle-HQ")
    LocationService->>LocationService: Search CSV cache
    LocationService-->>CLI: location_id = "93cb8a70-..."

    CLI->>NumberService: assign_number(user_id, phone, "callingPlan", location_id)

    NumberService->>GraphClient: post("/assignNumber", body)
    GraphClient->>GraphAPI: POST /assignNumber
    GraphAPI-->>GraphClient: 202 Accepted + Location header
    GraphClient-->>NumberService: operation_id from Location header

    loop Poll until complete (max 60s)
        NumberService->>GraphClient: get("/operations/{id}")
        GraphClient->>GraphAPI: GET /operations/{id}
        GraphAPI-->>GraphClient: {status: "running"}
        NumberService->>CLI: Update progress spinner
        NumberService->>NumberService: Wait 2-3 seconds
    end

    GraphAPI-->>GraphClient: {status: "succeeded"}
    GraphClient-->>NumberService: AsyncOperation (succeeded)
    NumberService-->>CLI: Success result

    CLI->>User: Assigned +12065551234 to john@contoso.com
```

### 7.4 Bulk Assign Flow

```mermaid
sequenceDiagram
    participant User
    participant CLI
    participant NumberService
    participant LocationService
    participant GraphClient
    participant GraphAPI as Microsoft Graph API

    User->>CLI: numbers bulk-assign users.csv --continue-on-error

    CLI->>CLI: Parse CSV file
    CLI->>CLI: Validate all rows (dry-run internally)

    loop For each row in CSV
        CLI->>LocationService: validate_location(row.location)
        LocationService-->>CLI: location_id or error
    end

    CLI->>User: Validated 50 rows, 2 warnings. Proceed? [Y/n]
    User->>CLI: Y

    loop For each valid row
        CLI->>NumberService: assign_number(...)
        NumberService->>GraphClient: post("/assignNumber", body)
        GraphClient->>GraphAPI: POST /assignNumber
        GraphAPI-->>GraphClient: 202 Accepted

        NumberService->>NumberService: poll_operation() until complete

        alt Success
            NumberService-->>CLI: Success
            CLI->>CLI: Update progress bar, log success
        else Failure
            NumberService-->>CLI: Error
            CLI->>CLI: Log error, continue (--continue-on-error)
        end
    end

    CLI->>CLI: Write results to bulk-assign-results.csv
    CLI->>User: Completed: 48 success, 2 failed. See results file.
```

### 7.5 Tenant Switch Flow

```mermaid
sequenceDiagram
    participant User
    participant CLI
    participant TenantService
    participant ConfigManager
    participant AuthService
    participant CacheManager

    User->>CLI: tenants switch fabrikam
    CLI->>TenantService: switch_tenant("fabrikam")

    TenantService->>ConfigManager: get_tenant("fabrikam")
    ConfigManager-->>TenantService: TenantProfile

    TenantService->>AuthService: is_authenticated("fabrikam")
    AuthService->>CacheManager: get_token("fabrikam")

    alt Token cached and valid
        CacheManager-->>AuthService: CachedToken
        AuthService-->>TenantService: true
    else Token missing or expired
        AuthService-->>TenantService: false
        TenantService->>AuthService: login(fabrikam_profile)
        AuthService-->>TenantService: AuthToken
    end

    TenantService->>ConfigManager: set_default_tenant("fabrikam")
    TenantService-->>CLI: TenantProfile

    CLI->>User: Switched to Fabrikam Inc (fabrikam)
```

### 7.6 Error Handling Flow (API Throttling)

```mermaid
sequenceDiagram
    participant CLI
    participant GraphClient
    participant GraphAPI as Microsoft Graph API

    CLI->>GraphClient: get("/admin/teams/userConfigurations")
    GraphClient->>GraphAPI: GET request
    GraphAPI-->>GraphClient: 429 Too Many Requests (Retry-After: 5)

    GraphClient->>GraphClient: Log warning, wait 5 seconds
    GraphClient->>GraphAPI: GET request (retry 1)
    GraphAPI-->>GraphClient: 429 Too Many Requests (Retry-After: 10)

    GraphClient->>GraphClient: Log warning, wait 10 seconds
    GraphClient->>GraphAPI: GET request (retry 2)
    GraphAPI-->>GraphClient: 200 OK + data

    GraphClient-->>CLI: Response data
```

---

## 8. Database Schema

This project uses **file-based storage only** - no traditional database.

### 8.1 Storage Architecture

```
~/.teams-phone/
├── config.toml              # Main configuration file
├── tokens/                  # Encrypted token cache
│   ├── contoso.json         # Per-tenant token cache
│   └── fabrikam.json
└── cache/                   # CSV cache from PowerShell export
    ├── locations.csv        # Emergency locations
    ├── policies.csv         # Policy definitions
    └── export-metadata.json # Cache metadata
```

### 8.2 Configuration Schema (TOML)

```toml
# ~/.teams-phone/config.toml

[default]
tenant = "contoso"           # Active tenant profile name
output_format = "table"      # Default output: table | json
color = true                 # Enable colored output
verbose = false              # Verbose logging

[cache]
users_ttl = 300              # User data cache TTL (seconds)
numbers_ttl = 300            # Number data cache TTL (seconds)
csv_stale_days = 30          # Warn if CSV older than this

[tenants.contoso]
tenant_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
client_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
display_name = "Contoso Ltd"
auth_method = "certificate"  # certificate | client-secret
cert_path = "/path/to/contoso-cert.pem"
default_location = "Seattle-HQ"

[tenants.fabrikam]
tenant_id = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
client_id = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
display_name = "Fabrikam Inc"
auth_method = "client-secret"
# Secret provided via TEAMS_PHONE_CLIENT_SECRET env var
default_location = "Portland-Main"
```

### 8.3 CSV Cache Schemas

#### locations.csv

| Column | Type | Description |
|--------|------|-------------|
| `locationId` | UUID | Unique location ID (used in API calls) |
| `civicAddressId` | UUID | Parent civic address ID |
| `description` | string | Location name/description |
| `companyName` | string | Company name |
| `address` | string | Full street address |
| `city` | string | City name |
| `stateOrProvince` | string | State/province code |
| `postalCode` | string | ZIP/postal code |
| `countryOrRegion` | string | Country code |
| `isDefault` | boolean | Default location flag |

#### policies.csv

| Column | Type | Description |
|--------|------|-------------|
| `policyType` | string | Policy type identifier |
| `policyName` | string | Policy name (Identity) |
| `policyId` | string | Policy ID for API calls |
| `description` | string | Policy description |
| `isGlobal` | boolean | Global/default policy flag |

#### export-metadata.json

```json
{
  "exportedAt": "2026-01-27T15:30:00Z",
  "exportedBy": "admin@contoso.com",
  "tenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "tenantName": "Contoso Ltd",
  "scriptVersion": "1.0.0",
  "counts": {
    "locations": 15,
    "policies": 23
  }
}
```

---

## 9. Source Tree

```
teams-phone-cli/
├── .github/
│   └── workflows/
│       ├── ci.yml                    # Lint, type-check, test on PR
│       ├── release.yml               # Build and publish to PyPI
│       └── contract-tests.yml        # Daily API contract validation
│
├── src/
│   └── teams_phone/                  # Main package
│       ├── __init__.py               # Package version, metadata
│       ├── __main__.py               # Entry point: python -m teams_phone
│       ├── cli/                      # CLI Layer (Typer commands)
│       │   ├── __init__.py
│       │   ├── main.py               # Root CLI app, global options
│       │   ├── users.py              # users command group
│       │   ├── numbers.py            # numbers command group
│       │   ├── policies.py           # policies command group
│       │   ├── locations.py          # locations command group
│       │   ├── tenants.py            # tenants command group
│       │   ├── auth.py               # auth command group
│       │   └── api_check.py          # api-check command
│       │
│       ├── services/                 # Service Layer (business logic)
│       │   ├── __init__.py
│       │   ├── user_service.py
│       │   ├── number_service.py
│       │   ├── policy_service.py
│       │   ├── location_service.py
│       │   ├── tenant_service.py
│       │   └── auth_service.py
│       │
│       ├── infrastructure/           # Infrastructure Layer
│       │   ├── __init__.py
│       │   ├── graph_client.py
│       │   ├── cache_manager.py
│       │   ├── config_manager.py
│       │   └── output_formatter.py
│       │
│       ├── models/                   # Pydantic models
│       │   ├── __init__.py
│       │   ├── user.py
│       │   ├── number.py
│       │   ├── policy.py
│       │   ├── location.py
│       │   ├── tenant.py
│       │   ├── auth.py
│       │   └── api_responses.py
│       │
│       ├── exceptions.py             # Custom exception hierarchy
│       ├── constants.py              # API URLs, exit codes, defaults
│       └── utils.py                  # Phone number normalization, helpers
│
├── tests/
│   ├── __init__.py
│   ├── conftest.py                   # Pytest fixtures
│   ├── unit/                         # Unit tests (mocked)
│   ├── integration/                  # Integration tests (real API)
│   ├── contract/                     # API contract tests
│   └── fixtures/                     # Test data files
│
├── scripts/
│   ├── Export-TeamsPhoneData.ps1     # PowerShell CSV export script
│   └── setup-test-tenant.ps1         # Test tenant setup script
│
├── docs/                             # Documentation
│
├── pyproject.toml                    # Project metadata, dependencies
├── uv.lock                           # Dependency lockfile
├── README.md
├── LICENSE
├── CHANGELOG.md
└── .pre-commit-config.yaml
```

---

## 10. Infrastructure and Deployment

### 10.1 Deployment Strategy

- **Strategy:** Package distribution via PyPI + optional standalone binaries
- **CI/CD Platform:** GitHub Actions
- **Primary:** PyPI distribution (MVP)
- **Future:** Standalone binaries via PyInstaller (Phase 5)

### 10.2 CI/CD Pipeline

| Workflow | Trigger | Actions |
|----------|---------|---------|
| `ci.yml` | PR, push to main | Lint, type-check, unit tests, build |
| `release.yml` | Version tag (v*) | Full tests, publish to PyPI, GitHub Release |
| `contract-tests.yml` | Daily 6:00 UTC | Validate Pydantic models against live API |

### 10.3 Environment Promotion Flow

```
Feature Branch → Main Branch → Test PyPI → PyPI (Production)
     │                │              │              │
     ▼                ▼              ▼              ▼
 Unit tests      Integration    Install test    Published
 Lint/format     Contract       Version bump    Available
 Type check      tests                          via pip
```

### 10.4 Rollback Strategy

- **Primary Method:** Version pinning (`pip install teams-phone-cli==1.0.0`)
- **Trigger Conditions:** Critical bug, API contract break, security vulnerability
- **Recovery Time Objective:** < 1 hour to publish hotfix

---

## 11. Error Handling Strategy

### 11.1 Exception Hierarchy

```
TeamsPhoneError (base)
├── AuthenticationError (exit 2)
│   ├── TokenExpiredError
│   ├── InvalidCredentialsError
│   └── CertificateError
├── AuthorizationError (exit 3)
│   └── InsufficientPermissionsError
├── NotFoundError (exit 4)
│   ├── UserNotFoundError
│   ├── NumberNotFoundError
│   ├── LocationNotFoundError
│   └── TenantNotFoundError
├── ValidationError (exit 5)
│   ├── InvalidPhoneNumberError
│   ├── E911LocationRequiredError
│   ├── InvalidPolicyError
│   └── CsvValidationError
├── RateLimitError (exit 6)
├── ConfigurationError (exit 7)
│   ├── MissingConfigError
│   └── InvalidConfigError
└── ContractError (exit 8)
    └── SchemaValidationError
```

### 11.2 Exit Codes

| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | General/unexpected error |
| 2 | Authentication error |
| 3 | Authorization error |
| 4 | Not found |
| 5 | Validation error |
| 6 | Rate limit exceeded |
| 7 | Configuration error |
| 8 | Contract/schema error |

### 11.3 Retry Policy

| HTTP Status | Retry? | Strategy |
|-------------|--------|----------|
| 429 (Throttled) | Yes | Use `Retry-After` header, max 3 retries |
| 500, 502, 503, 504 | Yes | Exponential backoff: 1s, 2s, 4s |
| 401 (Unauthorized) | Once | Refresh token, retry once |
| 400, 403, 404 | No | Fail immediately with user message |

### 11.4 Error Message Format

All user-facing errors include remediation guidance:

```
✗ Error: Emergency location required for Calling Plan number +12065551234

Calling Plan numbers require an emergency location for E911 compliance.

To fix:
  1. Run 'locations list' to see available locations
  2. Re-run with: numbers assign john@contoso.com +12065551234 --location "Seattle-HQ"

Or set a default location in your tenant config:
  tenants update contoso --default-location "Seattle-HQ"
```

---

## 12. Coding Standards

**MANDATORY FOR AI AGENTS**

### 12.1 Core Standards

- **Language:** Python 3.10+ (use modern syntax)
- **Linting:** Ruff (replaces flake8, black, isort)
- **Type Checking:** mypy in strict mode
- **Test Organization:** Tests mirror source in `tests/unit/`, `tests/integration/`

### 12.2 Critical Rules

| Rule | Description |
|------|-------------|
| **Use Pydantic for ALL API responses** | Never use raw dicts. Always validate with `extra='forbid'`. |
| **Never log secrets or tokens** | Use `[REDACTED]` placeholder. |
| **All errors must include remediation** | No bare "Error: something failed" messages. |
| **Phone numbers: normalize on input** | Use `utils.normalize_phone_number()`. |
| **Use services, not direct API calls** | CLI commands call services, never GraphClient directly. |
| **Async operations require polling** | `assignNumber`/`unassignNumber` return 202. Always poll. |
| **CSV cache is optional** | Services must work when CSV files are missing. |
| **Exit codes are mandatory** | Every error maps to a specific exit code. |
| **--json output must be valid JSON** | Output ONLY valid JSON to stdout when flag used. |
| **Tests must be independent** | No shared state between tests. |

### 12.3 Pydantic Model Standards

```python
from pydantic import BaseModel, Field, ConfigDict

class NumberAssignment(BaseModel):
    model_config = ConfigDict(
        extra="forbid",           # CRITICAL: Fail on unknown fields
        populate_by_name=True,    # Allow both snake_case and camelCase
    )

    id: str
    telephone_number: str = Field(alias="telephoneNumber")
    number_type: NumberType = Field(alias="numberType")
```

---

## 13. Test Strategy

### 13.1 Coverage Goals

| Layer | Target |
|-------|--------|
| Services | >90% |
| Infrastructure | >85% |
| CLI | >75% |
| Models | >95% |
| **Overall** | **>80%** |

### 13.2 Test Types

| Type | Location | Purpose |
|------|----------|---------|
| Unit | `tests/unit/` | Fast tests with mocked dependencies |
| Integration | `tests/integration/` | Real Graph API calls against test tenant |
| Contract | `tests/contract/` | Validate Pydantic models match live API |
| E2E | `tests/e2e/` | Full CLI command execution |

### 13.3 Test Markers

```bash
pytest -m unit                 # Fast unit tests only
pytest -m integration          # Integration tests (needs auth)
pytest -m contract             # API contract tests
pytest --cov=teams_phone       # With coverage report
```

---

## 14. Security

### 14.1 Authentication

- **Method:** Service principal (app registration)
- **Certificate auth preferred** over client secrets
- **Secrets via environment only** - never in config files

### 14.2 Secrets Management

| Data | Storage | Logging |
|------|---------|---------|
| Access tokens | Encrypted file cache | `[REDACTED]` |
| Client secrets | Environment variable | Never logged |
| Certificate keys | File system (user manages) | Never logged |

### 14.3 Required API Permissions

| Permission | Type | Purpose |
|------------|------|---------|
| `TeamsUserConfiguration.Read.All` | Application | Read user configs |
| `TeamsTelephoneNumber.Read.All` | Application | Read numbers |
| `TeamsTelephoneNumber.ReadWrite.All` | Application | Assign/unassign |
| `TeamsPolicyUserAssign.ReadWrite.All` | Application | Assign policies |
| `User.Read.All` | Application | User profiles |
| `Directory.Read.All` | Application | Tenant info |

### 14.4 Security Checklist for AI Agents

- [ ] No hardcoded secrets, tokens, or credentials
- [ ] All external input validated before use
- [ ] Sensitive data not logged at DEBUG level
- [ ] Error messages don't expose internal details
- [ ] File paths validated before read/write
- [ ] Subprocess calls use list args, not `shell=True`
- [ ] HTTP requests use HTTPS only
- [ ] Pydantic models validate all API responses

---

## 15. Next Steps

### For Projects with UI Components

N/A - This is a CLI-only project.

### For All Projects

1. **Review with Product Owner** - Validate architecture aligns with PRD goals
2. **Begin story implementation** - Use Dev agent with this architecture doc
3. **Set up infrastructure** - Configure test tenant, CI/CD pipelines

### Recommended Implementation Order

1. **Phase 1:** Foundation - Project setup, auth, tenant management
2. **Phase 2:** Read operations - Users, numbers, locations
3. **Phase 3:** Write operations - Assign, unassign, policies
4. **Phase 4:** Bulk operations - CSV import, progress bars
5. **Phase 5:** Polish - Documentation, release packaging

---

*Document generated by Architecture Document Workflow*
