# Task Recon: Implement get_user_policies method extracting effectivePolicyAssignments from UserConfiguration

**Task ID:** 24.2
**Status:** pending
**Generated:** 2026-01-29T21:15:00Z
**Agent:** claude-opus-4-5-20251101

---

## Task Summary

Add an async `get_user_policies` method to PolicyService that retrieves a user's effective policy assignments by fetching the UserConfiguration via GraphClient and extracting the `effectivePolicyAssignments` field.

### Parent Task Context

**Parent Task:** Implement PolicyService (ID: 24)
**Parent Goal:** Create the PolicyService for listing policies from CSV, showing user policy assignments, and assigning policies via Graph API.
**This Subtask's Role:** Provides the "show user policies" functionality by retrieving a user's currently assigned policies from the Graph API, enabling users to see what policies are in effect before making changes.

### Dependencies
- Depends on: 24.1 (Implement list_policies and validate_policy methods - DONE)

### Acceptance Criteria
From task details:
1. Method signature: `async def get_user_policies(self, user_id: str) -> list[EffectivePolicyAssignment]`
2. Fetch UserConfiguration from `/admin/teams/userConfigurations/{user_id}` via GraphClient
3. Validate response with `UserConfiguration.model_validate(response)`
4. Return the `effective_policy_assignments` list from UserConfiguration
5. Consider optional `policy_type` filter parameter
6. Handle NotFoundError appropriately (propagate from GraphClient)

---

## Insights from Previous Tasks

### Dependency Task Notes

| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 24.1 | Implement list_policies and validate_policy methods | Constructor takes both `graph_client` and `cache_manager`; graph_client is stored but unused in 24.1, reserved for this subtask |

### Reusable Patterns from Dependencies
- **Service constructor pattern**: PolicyService already has `self.graph_client` available from Task 24.1 implementation
- **Type hints with TYPE_CHECKING**: See `policy_service.py:16-17` for proper type-only imports to avoid circular dependencies

### Warnings and Gotchas
- **PolicyService differs from LocationService constructor**: LocationService only takes `cache_manager`; PolicyService takes both `graph_client` and `cache_manager` (see task_024.md notes)
- **Use GraphClient directly, not UserService**: Task details explicitly state to avoid UserService to prevent circular dependencies

### Open Items Inherited
- [x] Subtask 24.1: Implement `list_policies()` method - DONE
- [ ] Subtask 24.2: Implement `get_user_policies()` method - THIS TASK
- [ ] Subtask 24.3: Implement `assign_policy()` method with Graph API call

---

## Architecture Context

### Relevant Design Decisions
- Services abstract Graph API + CSV cache access (repository pattern)
- PolicyService needs GraphClient for async API operations (get_user_policies, assign_policy)
- Pydantic strict mode (`extra="forbid"`) for fail-fast on API contract changes
- NotFoundError propagates naturally from GraphClient for 404 responses

### Module Structure
```
src/teams_phone/
├── services/
│   └── policy_service.py  # Add get_user_policies method here
├── models/
│   └── user.py            # EffectivePolicyAssignment, UserConfiguration already defined
└── infrastructure/
    └── graph_client.py    # GraphClient.get() method for API calls
```

### Conventions to Follow
- Async methods for Graph API operations (use `async def`)
- Return type hints with list of Pydantic models
- Use `GraphClient.get()` which returns `dict[str, Any]` and raises appropriate exceptions
- Validate response with `Model.model_validate(response)` for Pydantic strict mode

---

## Related Code References

### Primary References (Must Read)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/services/policy_service.py` | 1-160 | Current PolicyService implementation - add method here |
| `src/teams_phone/models/user.py` | 66-80 | EffectivePolicyAssignment model definition |
| `src/teams_phone/models/user.py` | 82-121 | UserConfiguration model with effective_policy_assignments field |
| `src/teams_phone/services/user_service.py` | 106-127 | UserService.get_user() pattern to follow |

### Secondary References (Helpful Context)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/infrastructure/graph_client.py` | 367-395 | GraphClient.get() method signature and error handling |
| `src/teams_phone/models/__init__.py` | 29-35 | EffectivePolicyAssignment export for import |
| `.taskmaster/docs/graph-api-contracts/1-user-operations-verified.md` | 59-80 | Get User Configuration API contract |

### Code Patterns to Follow

```python
# Pattern from UserService.get_user() (src/teams_phone/services/user_service.py:106-127)
async def get_user(self, user_id: str) -> UserConfiguration:
    """Get a single user's voice configuration by ID."""
    response = await self.graph_client.get(
        f"/admin/teams/userConfigurations/{user_id}"
    )
    return UserConfiguration.model_validate(response)
```

```python
# Pattern for returning nested Pydantic model field
# The effective_policy_assignments field is already validated as part of UserConfiguration
# Simply access and return it after validation
user_config = UserConfiguration.model_validate(response)
return user_config.effective_policy_assignments  # Already list[EffectivePolicyAssignment]
```

---

## Dependencies

### Internal Modules
- `teams_phone.models.EffectivePolicyAssignment` - Return type model (already exported in models/__init__.py)
- `teams_phone.models.UserConfiguration` - For response validation
- `teams_phone.infrastructure.GraphClient` - Already imported via TYPE_CHECKING in policy_service.py

### External Packages
- None required (all dependencies already in use)

### New Dependencies Needed
- None required

---

## Testing Guidance

### Test File Location
`tests/unit/test_policy_service.py` (add new test class)

### Test Patterns in Use
```python
# Pattern from test_user_service.py for async GraphClient mocking (lines 410-422)
@pytest.mark.unit
@pytest.mark.asyncio
async def test_get_user_returns_user_configuration(
    self, mock_graph_client: MagicMock
) -> None:
    """get_user returns UserConfiguration object."""
    user_data = make_user_dict("user-123", "test@contoso.com")
    mock_graph_client.get = AsyncMock(return_value=user_data)

    service = UserService(mock_graph_client)
    user = await service.get_user("user-123")

    assert isinstance(user, UserConfiguration)
```

```python
# Pattern for NotFoundError propagation (test_user_service.py:442-452)
@pytest.mark.unit
@pytest.mark.asyncio
async def test_get_user_propagates_not_found_error(
    self, mock_graph_client: MagicMock
) -> None:
    """get_user propagates NotFoundError from GraphClient."""
    mock_graph_client.get = AsyncMock(
        side_effect=NotFoundError("User not found")
    )

    service = UserService(mock_graph_client)
    with pytest.raises(NotFoundError):
        await service.get_user("nonexistent-user")
```

### Fixtures Available
- `tmp_path` (pytest built-in) - For CacheManager with temp directory
- `mock_graph_client` fixture pattern from test_user_service.py

### Test Utilities
- `unittest.mock.AsyncMock` - For mocking async GraphClient methods
- `unittest.mock.MagicMock` - For GraphClient mock object

### Test Cases to Implement
1. `test_get_user_policies_returns_effective_assignments` - Successful retrieval returns list[EffectivePolicyAssignment]
2. `test_get_user_policies_empty_assignments` - User with no policies returns empty list
3. `test_get_user_policies_propagates_not_found_error` - Invalid user_id propagates NotFoundError
4. `test_get_user_policies_with_policy_type_filter` - If filter implemented, test filtering by policy_type
5. `test_get_user_policies_uses_correct_endpoint` - Verify correct API endpoint is called

---

## Risks and Blockers

### Technical Risks
- [x] Low risk: Simple method following established patterns from UserService.get_user()

### Dependency Risks
- [x] None: Subtask 24.1 is complete, GraphClient and models are ready

### Open Questions
- [ ] Should `policy_type` filter be added now or deferred to a later enhancement? Task details say "Consider adding optional policy_type filter parameter" - recommend implementing for completeness since it's a simple list comprehension

---

## Implementation Recommendations

### Suggested Approach
1. Add import for `EffectivePolicyAssignment` from `teams_phone.models` in policy_service.py
2. Implement `async def get_user_policies(self, user_id: str, policy_type: str | None = None) -> list[EffectivePolicyAssignment]`
3. Call `self.graph_client.get(f'/admin/teams/userConfigurations/{user_id}')`
4. Validate with `UserConfiguration.model_validate(response)`
5. Extract `effective_policy_assignments` field
6. Apply optional `policy_type` filter if provided (case-insensitive)
7. Return filtered list
8. Add unit tests in test_policy_service.py

### Files to Create/Modify
| Action | File | Reason |
|--------|------|--------|
| Modify | `src/teams_phone/services/policy_service.py` | Add get_user_policies method |
| Modify | `tests/unit/test_policy_service.py` | Add test class for get_user_policies |

### Quick Wins
- Start with minimal implementation (no filter) to validate the pattern
- Then add policy_type filter as a one-line list comprehension

---

## Cheat Sheet

### Key Imports
```python
# Add to policy_service.py imports
from teams_phone.models import EffectivePolicyAssignment, UserConfiguration
```

### Key Functions to Use
- `self.graph_client.get(endpoint: str) -> dict[str, Any]` - Fetch user configuration
- `UserConfiguration.model_validate(data) -> UserConfiguration` - Validate response
- `user_config.effective_policy_assignments` - Access policy assignments list (already `list[EffectivePolicyAssignment]`)

### Common Patterns
- **Async API method**: See `user_service.py:106-127` for get_user pattern
- **NotFoundError propagation**: GraphClient automatically raises NotFoundError on 404
- **Case-insensitive filter**: See `policy_service.py:76-80` for existing pattern

### API Endpoint
- **GET** `/admin/teams/userConfigurations/{user_id}`
- **Response** includes `effectivePolicyAssignments` array (aliased to `effective_policy_assignments` in Pydantic model)

### Response Structure Example
```json
{
  "effectivePolicyAssignments": [
    {
      "policyType": "TeamsCallingPolicy",
      "policyAssignment": {
        "displayName": "AllOn",
        "assignmentType": "group",
        "policyId": "f7593e81-772a-4455-88a3-fc9e5ebc1e4a",
        "groupId": "75ae6229-35fe-4b01-ae7f-7c50439d239c"
      }
    }
  ]
}
```

---

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