# Task Recon: Implement UserConfiguration and related nested models (TelephoneNumber, PolicyAssignment)

**Task ID:** 9.1
**Status:** pending
**Generated:** 2026-01-28T00:00:00Z
**Agent:** claude-opus-4-5-20251101

---

## Task Summary

Create `src/teams_phone/models/user.py` with strict Pydantic models for the Teams User Configuration Graph API response, including TelephoneNumber, PolicyAssignment, and AccountType enums. All models must use `ConfigDict(extra="forbid", populate_by_name=True)` for strict contract validation.

### Parent Task Context

**Parent Task:** Implement Pydantic Models for Graph API Responses (ID: 9)
**Parent Goal:** Create strict Pydantic models for all Microsoft Graph API response schemas with `extra='forbid'` for contract validation, enabling fail-fast behavior when the API changes unexpectedly.
**This Subtask's Role:** Implements the user-related models (UserConfiguration, TelephoneNumber, PolicyAssignment enums) which are foundational for all user operations in the CLI. Subsequent subtasks (9.2 and 9.3) depend on this subtask's patterns for NumberAssignment, AsyncOperation, and other models.

### Dependencies
- Depends on: Task 1 (Initialize Python Project Structure) - **DONE**
- Depends on: Task 2 (Implement Constants and Exception Hierarchy) - **DONE**

### Acceptance Criteria
1. Create `AccountType` enum with values: `user`, `resourceAccount`, `guest`, `sfbOnPremUser`, `unknown`
2. Create `TelephoneNumber` model with `telephone_number` and `assignment_category` fields (with camelCase aliases)
3. Create `PolicyAssignmentDetail` model with `display_name`, `assignment_type`, `policy_id`, `group_id` fields
4. Create `EffectivePolicyAssignment` model with `policy_type` and `policy_assignment` fields
5. Create `UserConfiguration` model with all fields from Graph API contract
6. All models use `ConfigDict(extra="forbid", populate_by_name=True)`
7. Update `__init__.py` to export all new models
8. Unit tests covering valid parsing, extra field rejection, alias mapping, and enum validation

---

## Insights from Previous Tasks

### Dependency Task Notes
| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 1 | Initialize Python Project Structure | Package structure uses `src/teams_phone/`, models go in `models/` subdirectory. mypy strict mode requires all functions have type hints. |
| 2 | Implement Constants and Exception Hierarchy | `ContractError` exception (exit code 8) is for API contract violations - will be raised when Pydantic validation fails on Graph API responses. |

### Reusable Patterns from Dependencies
- **ConfigDict pattern**: Already established in `models/auth.py`, `models/config.py`, `models/tenant.py` - use `model_config = ConfigDict(extra="forbid")`
- **StrEnum pattern**: Used in `models/auth.py:AuthStatusType` and `models/tenant.py:AuthMethod` - inherit from both `str` and `Enum`
- **Field with alias pattern**: See coding standards doc - `Field(alias="camelCase")` for Graph API field mapping

### Warnings and Gotchas
- **mypy strict mode**: All functions need complete type hints. The existing models demonstrate this pattern.
- **populate_by_name=True**: Allows both snake_case and camelCase for input validation - critical for testing where you might use either format.
- **StrEnum behavior**: Using `(str, Enum)` makes enum values usable as strings directly.

### Open Items Inherited
- [ ] From Task 2: Exception subclasses (TokenExpiredError, UserNotFoundError, etc.) to be added when specific features need them

---

## Architecture Context

### Relevant Design Decisions
- **Pydantic strict mode with extra='forbid'**: Defined in `.taskmaster/docs/architecture/12-coding-standards.md:30-41` - all Graph API responses must be validated through Pydantic models that reject unknown fields to detect API contract changes early.
- **Alias mapping**: Graph API uses camelCase, Python code uses snake_case. Use `Field(alias="camelCase")` and `populate_by_name=True`.

### Module Structure
```
src/teams_phone/models/
├── __init__.py          # Export all models
├── auth.py              # AuthStatus, CachedToken (existing)
├── cache.py             # CacheMetadata (existing)
├── config.py            # Config, CacheSettings, NetworkSettings (existing)
├── tenant.py            # TenantProfile, AuthMethod (existing)
└── user.py              # NEW: UserConfiguration, TelephoneNumber, etc.
```

### Conventions to Follow
- Model files named after the domain entity (e.g., `user.py`, `number.py`)
- All models inherit from `BaseModel`
- Use `ConfigDict(extra="forbid")` for strict validation
- Add `populate_by_name=True` when using aliases
- Module docstrings describe the purpose
- Class docstrings list attributes with types and descriptions

---

## Related Code References

### Primary References (Must Read)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/models/auth.py` | 1-79 | Established pattern for enums (AuthStatusType) and models with ConfigDict |
| `src/teams_phone/models/tenant.py` | 1-47 | StrEnum pattern (AuthMethod) and UUID field handling |
| `.taskmaster/docs/graph-api-contracts/1-user-operations-verified.md` | 26-52 | Exact Graph API response schema for UserConfiguration |
| `.taskmaster/docs/architecture/12-coding-standards.md` | 27-41 | Pydantic model standards (extra="forbid", populate_by_name) |

### Secondary References (Helpful Context)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/models/__init__.py` | 1-22 | Export pattern for models |
| `tests/unit/test_models.py` | 1-424 | Test patterns for Pydantic models (extra field rejection, enum validation) |
| `.taskmaster/docs/architecture/4-data-models.md` | 1-59 | Data model specifications for UserConfiguration and PolicyAssignment |

### Code Patterns to Follow

```python
# StrEnum pattern from src/teams_phone/models/auth.py:10-21
class AuthStatusType(str, Enum):
    """Authentication status states.

    Attributes:
        AUTHENTICATED: Successfully authenticated with valid token.
        UNAUTHENTICATED: No valid authentication (requires login).
        EXPIRED: Token has expired (requires refresh or re-authentication).
    """

    AUTHENTICATED = "authenticated"
    UNAUTHENTICATED = "unauthenticated"
    EXPIRED = "expired"
```

```python
# Model with ConfigDict pattern from src/teams_phone/models/auth.py:24-56
class CachedToken(BaseModel):
    """Cached authentication token for a tenant.

    Attributes:
        access_token: The OAuth 2.0 access token string.
        ...
    """

    model_config = ConfigDict(extra="forbid")

    access_token: str
    expires_at: datetime
    tenant_id: str
    scopes: list[str]
```

```python
# Model with alias pattern from coding standards (.taskmaster/docs/architecture/12-coding-standards.md:29-41)
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")
```

---

## Dependencies

### Internal Modules
- `pydantic.BaseModel` - Base class for all models
- `pydantic.ConfigDict` - Model configuration
- `pydantic.Field` - Field definitions with aliases
- `datetime` - For datetime fields (createdDateTime, modifiedDateTime)

### External Packages
- `pydantic>=2.0.0` - Already in pyproject.toml dependencies

### New Dependencies Needed
- None required

---

## Testing Guidance

### Test File Location
`tests/unit/test_models.py` (extend existing file) or `tests/unit/test_user_models.py` (new file)

### Test Patterns in Use
```python
# Extra field rejection pattern from tests/unit/test_models.py:107-118
@pytest.mark.unit
def test_extra_field_rejected(self) -> None:
    """TenantProfile rejects unknown fields (extra='forbid')."""
    with pytest.raises(ValidationError) as exc_info:
        TenantProfile(
            name="test",
            tenant_id=uuid4(),
            client_id=uuid4(),
            display_name="Test",
            auth_method=AuthMethod.CERTIFICATE,
            unknown_field="should fail",  # type: ignore[call-arg]
        )
    assert "extra_forbidden" in str(exc_info.value)
```

```python
# Enum validation pattern from tests/unit/test_models.py:133-144
@pytest.mark.unit
def test_invalid_auth_method_rejected(self) -> None:
    """TenantProfile rejects invalid auth method."""
    with pytest.raises(ValidationError) as exc_info:
        TenantProfile(
            name="test",
            tenant_id=uuid4(),
            client_id=uuid4(),
            display_name="Test",
            auth_method="invalid-method",  # type: ignore[arg-type]
        )
    assert "auth_method" in str(exc_info.value).lower()
```

### Fixtures Available
- None specific to these models - create sample JSON fixtures based on Graph API docs

### Test Utilities
- `tests/conftest.py` - pytest marker configuration (use `@pytest.mark.unit`)
- `pydantic.ValidationError` - For testing validation failures

### Required Test Cases (from task details)
1. Test valid parsing of complete UserConfiguration with all nested objects
2. Test parsing with minimal fields (empty lists for optional arrays)
3. Test that extra fields cause ValidationError (contract enforcement)
4. Test that missing required fields cause ValidationError
5. Test alias mapping (camelCase input -> snake_case attributes)
6. Test enum validation for AccountType

---

## Risks and Blockers

### Technical Risks
- [ ] **Nested model complexity**: EffectivePolicyAssignment contains nested PolicyAssignmentDetail - ensure proper nesting validation
- [ ] **datetime parsing**: Graph API returns ISO 8601 strings - Pydantic should handle this automatically but verify in tests

### Dependency Risks
- None - all dependencies (Tasks 1 and 2) are complete

### Open Questions
- [ ] Should `display_name` be optional in UserConfiguration? The architecture doc lists it as required but Graph API might return users without display names
- [ ] The task details show `tenant_id` in UserConfiguration but it's not in the sample response in the Graph API contracts doc - need to verify against actual API response

---

## Implementation Recommendations

### Suggested Approach
1. Create the enums first (AccountType) - simplest, establishes pattern
2. Create leaf models next (TelephoneNumber, PolicyAssignmentDetail) - no dependencies on other new models
3. Create intermediate models (EffectivePolicyAssignment) - depends on PolicyAssignmentDetail
4. Create main model (UserConfiguration) - depends on all above
5. Update `__init__.py` exports
6. Add unit tests for each model

### Files to Create/Modify
| Action | File | Reason |
|--------|------|--------|
| Create | `src/teams_phone/models/user.py` | Main file for user-related Pydantic models |
| Modify | `src/teams_phone/models/__init__.py` | Export new models |
| Modify | `tests/unit/test_models.py` | Add tests for new models (or create new test file) |

### Quick Wins
- Start with `AccountType` enum - fastest to implement and test
- Use existing test patterns from `test_models.py` - copy and adapt

---

## Cheat Sheet

### Key Imports
```python
from datetime import datetime
from enum import Enum
from typing import Optional

from pydantic import BaseModel, ConfigDict, Field
```

### Key Types to Define
```python
# Enum
class AccountType(str, Enum):
    USER = "user"
    RESOURCE_ACCOUNT = "resourceAccount"
    GUEST = "guest"
    SFB_ON_PREM_USER = "sfbOnPremUser"
    UNKNOWN = "unknown"

# Model with alias
class TelephoneNumber(BaseModel):
    model_config = ConfigDict(extra="forbid", populate_by_name=True)

    telephone_number: str = Field(alias="telephoneNumber")
    assignment_category: str = Field(alias="assignmentCategory")
```

### Common Patterns
- **StrEnum**: `class MyEnum(str, Enum):` - see `models/auth.py:10`
- **ConfigDict**: `model_config = ConfigDict(extra="forbid", populate_by_name=True)` - see coding standards
- **Field with alias**: `field_name: str = Field(alias="camelCase")` - see coding standards
- **Optional field**: `field_name: str | None = Field(alias="fieldName", default=None)`
- **List with default**: `items: list[Item] = Field(alias="items", default=[])`

### Graph API Response Schema (from contracts doc)
```json
{
  "id": "string (GUID)",
  "userPrincipalName": "string",
  "tenantId": "string",
  "displayName": "string",
  "accountType": "user|resourceAccount|guest|sfbOnPremUser|unknown",
  "isEnterpriseVoiceEnabled": "boolean",
  "featureTypes": ["Teams", "CallingPlan", ...],
  "telephoneNumbers": [
    {
      "telephoneNumber": "+13235533696",
      "assignmentCategory": "primary"
    }
  ],
  "effectivePolicyAssignments": [
    {
      "policyType": "TeamsCallingPolicy",
      "policyAssignment": {
        "displayName": "string",
        "assignmentType": "direct|group",
        "policyId": "string",
        "groupId": "string"
      }
    }
  ],
  "createdDateTime": "ISO 8601",
  "modifiedDateTime": "ISO 8601"
}
```

---

*Generated by task-recon (claude-opus-4-5-20251101) | Ready for implementation*
