# Task ID: 17

**Title:** Implement UserService

**Status:** pending

**Dependencies:** 7, 9

**Priority:** high

**Description:** Create the UserService for listing, searching, and retrieving user voice configurations from Graph API.

**Details:**

Create `src/teams_phone/services/user_service.py`:
```python
class UserService:
    def __init__(self, graph_client: GraphClient): ...
    
    async def list_users(
        self,
        licensed: bool | None = None,
        assigned: bool | None = None,
        account_type: AccountType | None = None,
        page_size: int = 100,
        max_items: int | None = None,
    ) -> list[UserConfiguration]: ...
    
    async def get_user(self, user_id: str) -> UserConfiguration: ...
    
    async def search_users(self, query: str) -> list[UserConfiguration]:
        """Search by display name, UPN, or phone number."""
        ...
    
    async def resolve_user(self, identifier: str) -> UserConfiguration:
        """Resolve user by UPN, display name, or object ID.
        Raise NotFoundError if not found.
        Raise ValidationError if ambiguous (multiple matches by name)."""
        ...
```

Use `/admin/teams/userConfigurations` endpoint.
Parse responses with Pydantic models (strict validation).

**Test Strategy:**

Unit tests with respx mocking Graph API responses: list with filters, pagination, user resolution logic, error handling for not found/ambiguous.

---

## Implementation Notes

### Session: 2026-01-29 19:15 - Agent: claude-opus-4-5-20251101

**Status:** Implemented

#### What Was Done
- Created `src/teams_phone/services/user_service.py` with UserService class
- Implemented all four methods: `list_users()`, `get_user()`, `search_users()`, `resolve_user()`
- Added comprehensive unit tests in `tests/unit/test_user_service.py` (29 tests)
- Used TYPE_CHECKING import for GraphClient to avoid circular import

#### Key Decisions Made
- **Circular import handling**: Used `TYPE_CHECKING` import for GraphClient in user_service.py and explicit import order in test file to avoid circular import between services and infrastructure packages. Did NOT export UserService from `services/__init__.py` - must import directly from `teams_phone.services.user_service`.
- **Filter syntax**: Used OData filter expressions matching Graph API documentation:
  - `isEnterpriseVoiceEnabled eq true/false` for licensed filter
  - `telephoneNumbers/$count gt 0` / `eq 0` for assigned filter
  - `accountType eq 'value'` for account type filter
- **Search detection**: Auto-detects query type in `search_users()`:
  - Phone numbers (starts with + or all digits): Searches telephoneNumbers collection
  - Email format (contains @): Exact UPN match
  - Other: Display name prefix match using startsWith()
- **Resolve logic**: `resolve_user()` detects identifier type:
  - GUID format: Direct lookup via get_user()
  - Email format: UPN search
  - Other: Display name search with disambiguation (raises ValidationError if multiple matches)

#### Patterns Discovered
- Async generator mocking pattern for `get_paginated()` - see test fixtures
- Pre-existing circular import fragility in codebase: `services.__init__` → `auth_service` → `infrastructure.__init__` → `graph_client` → `auth_service`

#### Open Items for Next Agent
- [ ] The UserService is NOT exported from `teams_phone.services` due to circular import - consider fixing the underlying import cycle in a future task
- [ ] Phone number search filter (`telephoneNumbers/any(t: t/telephoneNumber eq '...')`) needs verification against real Graph API

#### Gotchas and Warnings
- **Import order matters**: When importing UserService in tests or other modules, import from `teams_phone.infrastructure` first, then UserService, OR import directly from `teams_phone.services.user_service`
- **Page size capped at 999**: Graph API doesn't accept larger values
- Pre-existing test failure: `test_success_includes_checkmark` fails on Windows (Unicode issue, unrelated to this task)

---
