# Task ID: 19

**Title:** Implement NumberService Core Operations

**Status:** pending

**Dependencies:** 7, 9, 10

**Priority:** high

**Description:** Create the NumberService for listing, searching, and retrieving phone number inventory from Graph API.

**Details:**

Create `src/teams_phone/services/number_service.py`:
```python
class NumberService:
    def __init__(self, graph_client: GraphClient, location_service: LocationService): ...
    
    async def list_numbers(
        self,
        status: AssignmentStatus | None = None,
        number_type: NumberType | None = None,
        city: str | None = None,
        page_size: int = 100,
    ) -> list[NumberAssignment]: ...
    
    async def get_number(self, phone_number: str) -> NumberAssignment: ...
    
    async def search_numbers(self, pattern: str) -> list[NumberAssignment]:
        """Search by pattern: last 4 digits, area code, wildcards."""
        ...
```

Use `/admin/teams/telephoneNumberManagement/numberAssignments` endpoint.
Normalize phone numbers on input.
Support wildcard patterns: "206*", "*1234".

**Test Strategy:**

Unit tests with mocked Graph responses: list with filters, search patterns, number normalization, pagination handling.

---

## Implementation Notes

### Session: 2026-01-29 16:15 - Agent: claude-sonnet-4-5-20250929

**Subtask:** 19.1 - Implement list_numbers with status/type/city filters and pagination

**Status:** Implemented

#### What Was Done
- Created `src/teams_phone/services/number_service.py` with NumberService class
- Implemented `list_numbers` method with OData filtering for `assignmentStatus` and `numberType`
- Implemented client-side filtering for `city` (Graph API does not support OData city filter)
- Added pagination support via `page_size` and `max_items` parameters
- Updated `src/teams_phone/services/__init__.py` to document import path
- Created comprehensive unit tests in `tests/unit/test_number_service.py` (13 tests)

#### Key Decisions Made
- **City filter is client-side**: The Graph API explicitly does not support `$filter` on the `city` field. Followed the recon recommendation to filter client-side after fetching.
- **No LocationService dependency**: The task description mentioned LocationService but subtask 19.1 only requires GraphClient. LocationService may be added in future subtasks if needed.
- **Followed UserService pattern**: Method signature and implementation closely follow `UserService.list_users` for consistency.

#### Patterns Discovered
- OData filter format for numberAssignments: `assignmentStatus eq 'value'` (lowercase field name, single quotes around value)
- See `user_service.py:76-89` for the canonical filter construction pattern

#### Open Items for Next Agent
- [ ] 19.2: Implement `get_number` method
- [ ] 19.3: Implement `search_numbers` method with wildcard pattern support

#### Gotchas and Warnings
- **City filter performance**: When using city filter, all numbers matching OData filters are fetched first, then filtered client-side. For large inventories this may be slow.
- **max_items applies before city filter**: If using both `max_items` and `city`, the API limits happen before client-side filtering, so fewer results may be returned than expected.

---

### Session: 2026-01-29 19:35 - Agent: claude-sonnet-4-5-20250929

**Subtask:** 19.2 - Implement get_number with phone number normalization

**Status:** Implemented

#### What Was Done
- Added `get_number` method to `NumberService` in `src/teams_phone/services/number_service.py:104-137`
- Added imports for `NotFoundError` and `format_for_update` from utils
- Implemented phone number normalization using `format_for_update()` for E.164 format
- Query uses OData filter: `$filter=telephoneNumber eq '{normalized}'`
- Added 8 comprehensive unit tests in `tests/unit/test_number_service.py`

#### Key Decisions Made
- **Reuse format_for_update for normalization**: This function already handles all phone formats and returns E.164 with + prefix, exactly what the Graph API filter requires.
- **Use max_items=1**: Since we're fetching a single number, we limit the API to return at most one item for efficiency.
- **Show normalized number in error message**: When raising NotFoundError, display the normalized E.164 format so users see exactly what was searched.

#### Patterns Discovered
- OData filter for telephoneNumber: `telephoneNumber eq '+12065551234'` (E.164 format with + and single quotes)
- See `number_service.py:121-122` for filter construction
- Async iterator early return pattern: `async for item in ...: return` with exception after loop for not-found case

#### Test Cases Added
1. `test_get_number_returns_number_assignment` - Basic happy path
2. `test_get_number_normalizes_e164_input` - E.164 format preserved
3. `test_get_number_normalizes_digits_only_input` - Digits-only normalized to E.164
4. `test_get_number_normalizes_formatted_input` - Formatted input (+1-206-555-1234) normalized
5. `test_get_number_uses_correct_endpoint` - Verifies endpoint path
6. `test_get_number_uses_correct_filter` - Verifies filter construction
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 optimization

#### Open Items for Next Agent
- [ ] 19.3: Implement `search_numbers` method with wildcard pattern support (uses `utils.match_pattern`)

#### Gotchas and Warnings
- **Phone number must exist in inventory**: The method queries the inventory, not the user assignment endpoint. A phone number can be valid E.164 format but still not found if it's not in the tenant's inventory.
- **Normalization happens before API call**: Invalid phone numbers (< 10 digits after normalization) will raise `ValidationError` from `format_for_update`, not `NotFoundError`.

---

### Session: 2026-01-29 20:10 - Agent: claude-sonnet-4-5-20250929

**Subtask:** 19.3 - Implement search_numbers with wildcard pattern matching

**Status:** Implemented

#### What Was Done
- Added `search_numbers` method to `NumberService` in `src/teams_phone/services/number_service.py:141-183`
- Added import for `match_pattern` from utils
- Fetches all numbers via `list_numbers()`, then applies client-side pattern matching
- Supports wildcards: `206*` (prefix), `*1234` (suffix), `*555*` (contains), `1234` (substring)
- Added `max_results` parameter (default 100) with early exit optimization
- Created 12 comprehensive unit tests in `tests/unit/test_number_service.py`

#### Key Decisions Made
- **Client-side filtering only**: Graph API does not support wildcard filtering on telephoneNumber. All numbers are fetched first, then filtered using `match_pattern`.
- **Early exit on max_results**: To avoid unnecessary processing, the loop breaks as soon as `max_results` is reached.
- **Delegate to list_numbers**: Rather than duplicating pagination logic, `search_numbers` calls `list_numbers()` to fetch all numbers, keeping code DRY.
- **Default max_results=100**: Matches the recommended default from the task, balancing between returning useful results and performance.

#### Patterns Discovered
- `match_pattern` handles all pattern complexity: normalization, wildcard conversion to regex, substring matching
- See `utils.py:126-172` for pattern matching implementation details
- Empty pattern returns False per `match_pattern`, so empty pattern search returns no results

#### Test Cases Added
1. `test_search_numbers_prefix_pattern` - Matches `206*` pattern
2. `test_search_numbers_suffix_pattern` - Matches `*1234` pattern
3. `test_search_numbers_contains_pattern` - Matches `*555*` pattern
4. `test_search_numbers_exact_substring` - Matches `1234` (no wildcards)
5. `test_search_numbers_empty_pattern_returns_empty` - Empty pattern returns []
6. `test_search_numbers_no_matches` - No matching numbers returns []
7. `test_search_numbers_max_results_limits_output` - Respects max_results parameter
8. `test_search_numbers_default_max_results` - Default caps at 100 results
9. `test_search_numbers_wildcard_only_matches_all` - `*` matches all numbers
10. `test_search_numbers_returns_number_assignment_objects` - Returns proper model instances
11. `test_search_numbers_empty_inventory` - Empty inventory returns []
12. `test_search_numbers_calls_list_numbers` - Verifies endpoint used

#### Open Items for Next Agent
- Task 19 (parent) is now complete with all 3 subtasks implemented:
  - [x] 19.1: list_numbers with filters and pagination
  - [x] 19.2: get_number with normalization
  - [x] 19.3: search_numbers with wildcard patterns

#### Gotchas and Warnings
- **Performance for large inventories**: `search_numbers` fetches ALL numbers before filtering. For tenants with 10,000+ numbers, this could be slow. Consider documenting this in CLI help text.
- **Empty pattern returns empty list**: Unlike `*` which matches all, an empty pattern `""` returns no results per `match_pattern` behavior.
- **max_results is post-fetch**: The limit is applied after fetching all numbers, so API calls are not reduced by setting a lower max_results.

