# Task Recon: Implement get_number with phone number normalization

**Task ID:** 19.2
**Status:** pending
**Generated:** 2026-01-29T19:30:00Z
**Agent:** claude-opus-4-5-20251101

---

## Task Summary

Create the `get_number` method in NumberService to retrieve a single phone number assignment by phone number with input normalization. The method should accept phone numbers in any format, normalize them using `utils.format_for_update()` to E.164 format, query the Graph API with an OData filter, and return a `NumberAssignment` object. If no matching number is found, raise `NotFoundError` with appropriate remediation guidance.

### Parent Task Context

**Parent Task:** Implement NumberService Core Operations (ID: 19)
**Parent Goal:** Create the NumberService for listing, searching, and retrieving phone number inventory from Graph API. Provides `list_numbers`, `get_number`, and `search_numbers` methods.
**This Subtask's Role:** Implements the single-number retrieval functionality with phone number normalization, which is essential for commands like `numbers show <number>`.

### Dependencies
- Depends on: 19.1 (list_numbers implementation - DONE)

### Acceptance Criteria
1. Accept `phone_number` parameter in any format (E.164, digits-only, formatted)
2. Normalize input using `utils.format_for_update()` to get E.164 format with + prefix
3. Query API using: `/admin/teams/telephoneNumberManagement/numberAssignments?$filter=telephoneNumber eq '{normalized}'`
4. Use `GraphClient.get_paginated()` with `max_items=1` to retrieve single result
5. Raise `NotFoundError` if no matching number found with remediation
6. Validate response against `NumberAssignment.model_validate()`
7. Return `NumberAssignment`

---

## Insights from Previous Tasks

### Dependency Task Notes

| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 19.1 | Implement list_numbers | City filter is client-side only (Graph API limitation); OData filter format is `assignmentStatus eq 'value'` with lowercase field names and single quotes |
| 19 | NumberService Core Operations | Endpoint is `/admin/teams/telephoneNumberManagement/numberAssignments`; phone number normalization should use utils functions |

### Reusable Patterns from Dependencies
- **OData filter construction**: Implemented in `number_service.py:82-88` during Task 19.1
- **Filter format for telephoneNumber**: `$filter=telephoneNumber eq '+12052582895'` per Graph API docs
- **Method signature pattern**: Follow `list_numbers` style with `*` for keyword-only args

### Warnings and Gotchas
- **From Task 19.1**: Graph API does not support filtering by `id` or `city` - only `telephoneNumber`, `assignmentStatus`, `numberType`, and `capabilities`
- **Phone number format for filter**: Must be E.164 format with `+` prefix for the filter query

### Open Items Inherited
- None from 19.1 that affect this subtask

---

## Architecture Context

### Relevant Design Decisions
- **Service Layer Pattern**: Services abstract Graph API access and provide business logic
- **Repository Pattern**: Services use GraphClient for all API communication
- **Fail-fast validation**: Use Pydantic strict mode for API contract validation

### Module Structure
```
src/teams_phone/
├── services/
│   └── number_service.py   # NumberService class (add get_number here)
├── utils.py                # format_for_update() function for normalization
├── exceptions.py           # NotFoundError with remediation support
└── models/
    └── number.py           # NumberAssignment Pydantic model
```

### Conventions to Follow
- Use `async def` for all service methods
- Include docstrings with Args, Returns, Raises sections
- Use type hints for all parameters and return types
- Follow existing import patterns (TYPE_CHECKING for GraphClient)

---

## Related Code References

### Primary References (Must Read)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/services/number_service.py` | 35-101 | Existing `list_numbers` method - follow this pattern for `get_number` |
| `src/teams_phone/utils.py` | 67-88 | `format_for_update()` function for E.164 normalization |
| `src/teams_phone/services/user_service.py` | 100-120 | `get_user` method pattern (single item retrieval) |
| `src/teams_phone/exceptions.py` | 72-82 | `NotFoundError` class definition with remediation |

### Secondary References (Helpful Context)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/models/number.py` | 132-185 | `NumberAssignment` model with all fields |
| `src/teams_phone/infrastructure/graph_client.py` | 426-489 | `get_paginated()` async iterator |
| `.taskmaster/docs/graph-api-contracts/2-phone-number-operations-verified.md` | 21-26 | OData filter examples for telephoneNumber |

### Code Patterns to Follow

```python
# Example pattern from UserService.get_user (user_service.py:100-120)
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
# Phone number normalization pattern (utils.py:67-88)
def format_for_update(number: str) -> str:
    """Format a phone number for the updateNumber API endpoint."""
    digits = normalize_phone_number(number)
    return f"+{digits}"
```

```python
# Filter pattern for telephoneNumber (from Graph API docs)
# $filter=telephoneNumber eq '+12065551234'
```

---

## Dependencies

### Internal Modules
- `teams_phone.utils.format_for_update` - Normalizes any phone format to E.164 with + prefix
- `teams_phone.exceptions.NotFoundError` - Exception with remediation for missing numbers
- `teams_phone.models.NumberAssignment` - Pydantic model for validation
- `teams_phone.infrastructure.graph_client.GraphClient` - API communication

### External Packages
- `pydantic` - Already in use for model validation

### New Dependencies Needed
- None required

---

## Testing Guidance

### Test File Location
`tests/unit/test_number_service.py` (extend existing file)

### Test Patterns in Use
```python
# Example test structure from test_number_service.py:93-118
@pytest.mark.unit
@pytest.mark.asyncio
async def test_list_numbers_returns_number_assignments(
    self, mock_graph_client: MagicMock
) -> None:
    """list_numbers returns list of NumberAssignment objects."""
    number_data = [
        make_number_dict("num-1", "+14255551001"),
    ]

    async def mock_paginated(
        *args: Any, **kwargs: Any
    ) -> AsyncIterator[dict[str, Any]]:
        for item in number_data:
            yield item

    mock_graph_client.get_paginated = mock_paginated
    service = NumberService(mock_graph_client)
    numbers = await service.list_numbers()
    # assertions...
```

```python
# NotFoundError test pattern from test_user_service.py:597-614
@pytest.mark.unit
@pytest.mark.asyncio
async def test_resolve_user_by_upn_not_found(
    self, mock_graph_client: MagicMock
) -> None:
    """resolve_user raises NotFoundError when UPN not found."""
    async def mock_paginated(
        *args: Any, **kwargs: Any
    ) -> AsyncIterator[dict[str, Any]]:
        return
        yield  # pragma: no cover

    mock_graph_client.get_paginated = mock_paginated
    service = UserService(mock_graph_client)
    with pytest.raises(NotFoundError) as exc_info:
        await service.resolve_user("nonexistent@contoso.com")
    assert "nonexistent@contoso.com" in str(exc_info.value)
```

### Fixtures Available
- `mock_graph_client` fixture in `test_number_service.py:53-56`
- `make_number_dict` helper function for creating test data

### Test Utilities
- `tests/conftest.py` - pytest markers: `@pytest.mark.unit`, `@pytest.mark.asyncio`

### Test Cases to Implement
1. `test_get_number_returns_number_assignment` - Returns NumberAssignment object
2. `test_get_number_normalizes_e164_input` - E.164 format input (+12065551234)
3. `test_get_number_normalizes_digits_only_input` - Digits only (12065551234)
4. `test_get_number_normalizes_formatted_input` - Formatted (+1-206-555-1234)
5. `test_get_number_uses_correct_endpoint` - Verifies endpoint path
6. `test_get_number_uses_correct_filter` - Verifies `$filter=telephoneNumber eq '...'`
7. `test_get_number_raises_not_found_for_missing_number` - NotFoundError with remediation
8. `test_get_number_passes_max_items_one` - Verifies max_items=1 passed to get_paginated

---

## Risks and Blockers

### Technical Risks
- [ ] **Phone number format edge cases**: Different international formats may need testing; `format_for_update` handles this but verify with non-US numbers

### Dependency Risks
- None - all dependencies (19.1) are complete

### Open Questions
- None identified - task requirements are clear

---

## Implementation Recommendations

### Suggested Approach
1. Add import for `format_for_update` from `teams_phone.utils`
2. Add import for `NotFoundError` from `teams_phone.exceptions`
3. Implement `get_number` method following the pattern below
4. Add comprehensive unit tests for normalization and error cases
5. Run `pytest tests/unit/test_number_service.py -v` to verify
6. Run `ruff check src/ tests/` and `mypy src/ tests/` for quality checks

### Files to Create/Modify
| Action | File | Reason |
|--------|------|--------|
| Modify | `src/teams_phone/services/number_service.py` | Add get_number method |
| Modify | `tests/unit/test_number_service.py` | Add tests for get_number |

### Quick Wins
- Method is similar to existing patterns - just needs filter construction and error handling
- Reuse `make_number_dict` helper for tests

---

## Cheat Sheet

### Key Imports
```python
from teams_phone.exceptions import NotFoundError
from teams_phone.utils import format_for_update
```

### Key Functions to Use
- `format_for_update(number)` - Normalizes to E.164 format with + prefix
- `self.graph_client.get_paginated(endpoint, params=params, max_items=1)` - Single item fetch
- `NumberAssignment.model_validate(item)` - Pydantic validation
- `NotFoundError(message, remediation=...)` - Exception with guidance

### Implementation Template
```python
async def get_number(self, phone_number: str) -> NumberAssignment:
    """Get a single phone number assignment by phone number.

    Args:
        phone_number: Phone number in any format (E.164, digits-only, formatted).

    Returns:
        NumberAssignment for the specified phone number.

    Raises:
        NotFoundError: If the phone number is not found in inventory.
        ValidationError: If the phone number format is invalid.
        ContractError: If API response doesn't match expected schema.
    """
    normalized = format_for_update(phone_number)
    params = {"$filter": f"telephoneNumber eq '{normalized}'"}

    async for item in self.graph_client.get_paginated(
        "/admin/teams/telephoneNumberManagement/numberAssignments",
        params=params,
        max_items=1,
    ):
        return NumberAssignment.model_validate(item)

    raise NotFoundError(
        f"Phone number '{normalized}' not found",
        remediation=(
            "Verify the phone number is in your inventory. "
            "Use `teams-phone numbers list` to see available numbers."
        ),
    )
```

### Common Patterns
- OData filter: `$filter=telephoneNumber eq '+12065551234'`
- E.164 format: `+` followed by country code and number (e.g., `+12065551234`)
- Async iterator pattern: `async for item in ...` with early return

---

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