# Task ID: 9

**Title:** Implement Pydantic Models for Graph API Responses

**Status:** pending

**Dependencies:** 1, 2

**Priority:** high

**Description:** Create strict Pydantic models for all Microsoft Graph API response schemas with extra='forbid' for contract validation.

**Details:**

Create models in `src/teams_phone/models/`:

`user.py`:
```python
class UserConfiguration(BaseModel):
    model_config = ConfigDict(extra="forbid", populate_by_name=True)
    id: str
    user_principal_name: str = Field(alias="userPrincipalName")
    display_name: str = Field(alias="displayName")
    account_type: AccountType = Field(alias="accountType")
    is_enterprise_voice_enabled: bool = Field(alias="isEnterpriseVoiceEnabled")
    feature_types: list[str] = Field(alias="featureTypes")
    telephone_numbers: list[TelephoneNumber] = Field(alias="telephoneNumbers", default=[])
    effective_policy_assignments: list[PolicyAssignment] = Field(alias="effectivePolicyAssignments", default=[])
```

`number.py`: NumberAssignment, AsyncOperation, OperationNumber
`policy.py`: PolicyAssignment, Policy (CSV)
`location.py`: EmergencyLocation (CSV)
`api_responses.py`: GraphListResponse, GraphErrorResponse

**Test Strategy:**

Unit tests with sample JSON fixtures from Graph API docs: verify parsing succeeds for valid data, fails with ValidationError for unknown fields or missing required fields.

---

## Implementation Notes

### Session: 2026-01-28 - Agent: claude-sonnet-4-5-20250929

**Subtask:** 9.1 - Implement UserConfiguration and related nested models

**Status:** Implemented

#### What Was Done
- Created `src/teams_phone/models/user.py` with 5 models:
  - `AccountType` enum: user, resourceAccount, guest, sfbOnPremUser, unknown
  - `TelephoneNumber` model: telephoneNumber, assignmentCategory
  - `PolicyAssignmentDetail` model: displayName, assignmentType, policyId, groupId
  - `EffectivePolicyAssignment` model: policyType, policyAssignment (nested)
  - `UserConfiguration` model: complete user config with all nested models
- Updated `src/teams_phone/models/__init__.py` to export all new models
- Added 28 new unit tests in `tests/unit/test_models.py` (total 168 tests pass)

#### Key Decisions Made
- **display_name made optional**: The architecture doc lists it as required but Graph API may return users without display names (e.g., resource accounts). Made it `Optional[str]` with `default=None`.
- **tenant_id kept as str**: The task details showed tenant_id in UserConfiguration which is present in the Graph API response schema. Kept as plain str rather than UUID for flexibility.
- **datetime fields made optional**: createdDateTime and modifiedDateTime may not always be present in all responses, so made them optional.
- **Test strategy for populate_by_name**: mypy in strict mode doesn't understand `populate_by_name=True`, so tests use camelCase (alias) for direct construction and `model_validate()` for snake_case testing.

#### Patterns Discovered
- **StrEnum pattern**: Use `(str, Enum)` base classes - see `models/auth.py:10` and `models/user.py:14`
- **ConfigDict with alias**: Always use `ConfigDict(extra="forbid", populate_by_name=True)` for Graph API models
- **Field alias pattern**: `field_name: Type = Field(alias="camelCase")` - attribute access uses snake_case, serialization can use either
- **Testing aliases**: mypy only recognizes alias names, so use alias in tests or use `model_validate()` with dicts

#### Open Items for Next Agent
- [x] Subtask 9.2: Implement NumberAssignment, AsyncOperation, OperationNumber models (DONE)
- [ ] Subtask 9.3: Implement PolicyAssignment, Policy, EmergencyLocation, GraphListResponse models

#### Gotchas and Warnings
- **mypy strict mode + aliases**: When using `populate_by_name=True`, mypy doesn't recognize snake_case constructor args. Either use camelCase aliases in tests or use `model_validate()` with dicts.
- **Nested model validation**: When passing dicts for nested models (like policyAssignment), Pydantic automatically validates them. Use `model_validate()` for dict-based construction in tests.
- **Test assertion with aliases**: Error messages from Pydantic use the alias (camelCase), not the field name. E.g., "accounttype" not "account_type" in validation errors.

---

### Session: 2026-01-28 - Agent: claude-sonnet-4-5-20250929

**Subtask:** 9.2 - Implement NumberAssignment and AsyncOperation models

**Status:** Implemented

#### What Was Done
- Created `src/teams_phone/models/number.py` with 5 enums and 3 models:
  - `NumberType` enum: directRouting, callingPlan, operatorConnect
  - `ActivationState` enum: activated, assignmentPending, assignmentFailed, updatePending, updateFailed
  - `AssignmentStatus` enum: unassigned, userAssigned, conferenceAssigned, voiceApplicationAssigned
  - `AssignmentCategory` enum: primary, private, alternate
  - `OperationStatus` enum: notStarted, running, succeeded, failed, skipped, unknownFutureValue
  - `OperationNumber` model: resourceLocation, status (for async op results)
  - `AsyncOperation` model: id, createdDateTime, status, numbers, @odata.type
  - `NumberAssignment` model: complete phone number inventory with all 17 fields
- Updated `src/teams_phone/models/__init__.py` to export all 8 new items
- Added 54 new unit tests (total now 116 tests in test_models.py)

#### Key Decisions Made
- **@odata.type alias**: Used `Field(alias="@odata.type")` for the OData type annotation - Pydantic handles the `@` character correctly in aliases
- **datetime as string input**: Pydantic automatically parses ISO 8601 strings to datetime objects. Tests use string literals with `# type: ignore[arg-type]` for mypy
- **capabilities as list[str]**: Not an enum since the values aren't well-defined in the API docs (userAssignment, conferenceAssignment, etc. are examples)
- **All optional fields have defaults**: Optional fields that may be null from the API default to `None`, list fields default to `[]`

#### Patterns Discovered
- **@odata.type pattern**: `odata_type: Optional[str] = Field(alias="@odata.type", default=None)` - see `models/number.py:102`
- **datetime with alias**: `created_date_time: datetime = Field(alias="createdDateTime")` - Pydantic auto-parses ISO strings
- **Nested list model**: `numbers: list[OperationNumber] = Field(default=[])` - validates each item in the list

#### Open Items for Next Agent
- [x] Subtask 9.3: Implement Policy, EmergencyLocation, GraphListResponse models (DONE)

#### Gotchas and Warnings
- **datetime string input in tests**: When testing datetime parsing, use string values with `# type: ignore[arg-type]` since mypy expects datetime objects
- **dict type annotation for model_validate tests**: When using `model_validate()` with dicts containing None values, annotate as `dict[str, object]` to satisfy mypy

---

### Session: 2026-01-28 - Agent: claude-sonnet-4-5-20250929

**Subtask:** 9.3 - Implement Policy, EmergencyLocation, GraphListResponse models

**Status:** Implemented

#### What Was Done
- Created `src/teams_phone/models/policy.py` with Policy model:
  - Represents voice policy data from CSV cache (exported via PowerShell)
  - Fields: policy_type, policy_name, policy_id, description (optional), is_global
  - Uses camelCase aliases to match CSV column names from PowerShell export
- Created `src/teams_phone/models/location.py` with EmergencyLocation model:
  - Represents emergency calling locations from CSV cache
  - Fields: location_id, civic_address_id, description, company_name (optional), address (optional), city (optional), state_or_province (optional), postal_code (optional), country_or_region (optional), is_default
  - Uses camelCase aliases matching PowerShell export column names
- Created `src/teams_phone/models/api_responses.py` with 3 models:
  - `GraphErrorDetail`: Self-referential model for nested API error details (code, message, target, details)
  - `GraphErrorResponse`: Wrapper model for Graph API error responses
  - `GraphListResponse[T]`: Generic model for paginated list responses with @odata.context, @odata.count, @odata.nextLink, value
- Updated `src/teams_phone/models/__init__.py` to export all 5 new models
- Added 33 new unit tests (total now 149 tests in test_models.py)

#### Key Decisions Made
- **CSV models use aliases**: Initially the recon suggested CSV models don't need aliases, but the PowerShell export script uses camelCase column names (e.g., `policyType`, `locationId`), so added aliases with `populate_by_name=True` for flexibility
- **Generic GraphListResponse**: Used Pydantic's `Generic[T]` support - `GraphListResponse[UserConfiguration]` and `GraphListResponse[NumberAssignment]` both work for type-safe parsing
- **Self-referential GraphErrorDetail**: Used forward reference `list["GraphErrorDetail"]` for the nested details field
- **OData aliases**: Used `Field(alias="@odata.context")` pattern for all @odata.* fields in GraphListResponse

#### Patterns Discovered
- **Generic Pydantic models**: `class GraphListResponse(BaseModel, Generic[T]):` with `value: list[T]` - see `models/api_responses.py:44`
- **Self-referential forward refs**: `details: list["GraphErrorDetail"] | None = None` - Python handles forward references automatically in Pydantic v2
- **CSV model pattern**: Same as Graph API models - use `ConfigDict(extra="forbid", populate_by_name=True)` with aliases matching source column names

#### Open Items for Next Agent
- Task 9 parent task is now complete - all subtasks (9.1, 9.2, 9.3) are done
- Consider creating services that use these models for data parsing

#### Gotchas and Warnings
- **Generic type construction in tests**: Use `GraphListResponse[UserConfiguration].model_validate({...})` - the generic type parameter must be specified at validation time
- **Nested generic validation**: When validating GraphListResponse, the inner items are also validated against the type parameter (e.g., UserConfiguration), so extra fields in items will fail

---
