# Task Recon: Implement operation ID extraction from Location header

**Task ID:** 20.2
**Status:** pending
**Generated:** 2026-01-29
**Agent:** claude-opus-4-5-20251101

---

## Task Summary

Add a private `_extract_operation_id` helper method to the `NumberService` class that parses the operation ID from the `Location` header returned by 202 Accepted responses for assign/unassign operations.

### Parent Task Context

**Parent Task:** Implement Async Operation Polling (ID: 20)
**Parent Goal:** Add async operation polling to NumberService for assign/unassign operations that return 202 Accepted. This enables the CLI to track the status of long-running phone number assignment/unassignment operations.
**This Subtask's Role:** Implement the operation ID extraction helper that will be used by future assign/unassign methods to parse the Location header and get the operation ID needed for polling. This is a supporting utility that enables Task 20's polling mechanism to work with real API responses.

### Dependencies
- Depends on: 20.1 (poll_operation method) - **COMPLETED**

### Acceptance Criteria
1. Create private `_extract_operation_id(self, location_header: str) -> str` method
2. Parse Location header format: `.../operations('{operation_id}')`
3. Use regex pattern `r"operations\('([^']+)'\)"` to extract the GUID
4. Raise `ValidationError` with descriptive message if header format doesn't match
5. Handle edge cases: query params in URL, full URL with base path, URL-encoded characters
6. Add type hints and docstring following existing codebase conventions

---

## Insights from Previous Tasks

### Dependency Task Notes

| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 20.1 | Implement poll_operation method | Uses `graph_client` (public attribute, not `_graph_client`). Terminal states: succeeded, failed, skipped. Endpoint uses `/{operationId}` directly. |
| 19 | NumberService Core Operations | Service pattern established with GraphClient injection. All methods use comprehensive docstrings with Args/Returns/Raises. |

### Reusable Patterns from Dependencies
- **ValidationError usage**: Already imported in services - see `user_service.py:12` for import pattern
- **Regex pattern matching**: See `utils.py:166-172` for regex pattern construction and matching
- **Private method conventions**: Use `_` prefix for internal helper methods

### Warnings and Gotchas
- **Location header format**: The header contains `operations('{operation_id}')` with OData-style parentheses and single quotes around the ID
- **Endpoint format differs**: The polling endpoint uses `/{operationId}` directly (no parentheses), but the Location header uses OData format `('id')`
- **Operation ID may be base64 encoded**: Per API docs in `2-phone-number-operations-verified.md:239`, the operation ID is base64 encoded

### Open Items Inherited
- From 20.1: This method will be consumed by future assign/unassign operations to extract operation IDs from their 202 responses

---

## Architecture Context

### Relevant Design Decisions
- Services raise domain exceptions (`ValidationError`, `NotFoundError`) - never raw Python exceptions
- All errors must include remediation guidance per coding standards
- Use `re` module for regex operations (already imported in related files)

### Module Structure
```
src/teams_phone/services/number_service.py  <- Add _extract_operation_id here
src/teams_phone/exceptions.py               <- ValidationError class
src/teams_phone/utils.py                    <- Reference for regex patterns
```

### Conventions to Follow
- Use `from __future__ import annotations` at top of file (already present)
- Private methods prefixed with `_`
- Comprehensive docstrings with Args/Returns/Raises sections
- Type hints on all parameters and return values

---

## Related Code References

### Primary References (Must Read)

| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/services/number_service.py` | 1-250 | Current NumberService - add _extract_operation_id here after poll_operation |
| `src/teams_phone/exceptions.py` | 85-95 | ValidationError class definition and usage |
| `src/teams_phone/utils.py` | 166-172 | Regex pattern matching example in match_pattern() |
| `.taskmaster/docs/graph-api-contracts/2-phone-number-operations-verified.md` | 115, 175, 269 | Location header format examples |

### Secondary References (Helpful Context)

| File | Lines | Purpose |
|------|-------|---------|
| `.taskmaster/docs/graph-api-contracts/implementation-notes.md` | 5-12 | Polling pattern overview |
| `src/teams_phone/services/user_service.py` | 12, 192-244 | ValidationError import and usage example |
| `tests/unit/test_number_service.py` | 22-53 | Test helper pattern to follow |

### Code Patterns to Follow

```python
# Regex pattern with group capture from utils.py:168
regex_pattern = re.escape(normalized_pattern).replace(r"\*", ".*")
return bool(re.search(regex_pattern, digits))
```

```python
# ValidationError usage from user_service.py:242-247
raise ValidationError(
    f"Ambiguous identifier '{identifier}' matches multiple users: "
    f"{', '.join(names)}",
    remediation="Use 'UPN' or 'objectId' to uniquely identify the user.",
)
```

```python
# Private method pattern
def _extract_operation_id(self, location_header: str) -> str:
    """Extract operation ID from Location header.

    Args:
        location_header: Location header value from 202 response.

    Returns:
        The extracted operation ID string.

    Raises:
        ValidationError: If header format doesn't match expected pattern.
    """
```

---

## Dependencies

### Internal Modules
- `teams_phone.exceptions.ValidationError` - For raising errors on malformed headers
- `re` (stdlib) - For regex pattern matching (need to add import)

### External Packages
- None - only stdlib `re` module needed

### New Dependencies Needed
- `import re` - Add to number_service.py imports (not currently present)

---

## Testing Guidance

### Test File Location
`tests/unit/test_number_service.py` - Add new test class `TestNumberServiceExtractOperationId`

### Test Patterns in Use

```python
# Test class pattern from test_number_service.py:99-108
class TestNumberServiceInit:
    """Tests for NumberService initialization."""

    @pytest.mark.unit
    def test_accepts_graph_client(self, mock_graph_client: MagicMock) -> None:
        """NumberService accepts GraphClient in constructor."""
        service = NumberService(mock_graph_client)
        assert service.graph_client is mock_graph_client
```

### Fixtures Available
- `mock_graph_client` - MagicMock for GraphClient (existing fixture)

### Test Utilities
- No new helpers needed - direct method calls on service instance

### Required Test Cases (from task spec)
1. Successful extraction from valid header with just operation ID
2. Successful extraction from full URL with base path
3. Successful extraction with query parameters after operation ID
4. ValidationError raised for malformed header (missing ID)
5. ValidationError raised for wrong format (no quotes)
6. ValidationError raised for empty string
7. Edge case: URL-encoded characters in operation ID (if applicable)

---

## Risks and Blockers

### Technical Risks
- [ ] **Regex edge cases**: Operation IDs may contain special characters that could break regex - test with actual Graph API response samples
- [ ] **URL encoding**: If operation ID contains URL-encoded characters, may need to decode before returning

### Dependency Risks
- None - Task 20.1 (poll_operation) is complete and working

### Open Questions
- [ ] Should URL-encoded characters in operation ID be decoded? Task spec doesn't specify - recommend returning as-is since the polling endpoint should accept the same format
- [ ] Should the method handle both OData format `operations('id')` and direct path format `operations/id`? API docs show only OData format in Location header

---

## Implementation Recommendations

### Suggested Approach
1. Add `import re` to number_service.py imports
2. Verify ValidationError is already imported (it is, via NotFoundError pattern)
3. Implement `_extract_operation_id` method after `poll_operation`
4. Use regex pattern `r"operations\('([^']+)'\)"` to extract ID
5. Raise ValidationError with clear message and remediation if no match
6. Write comprehensive unit tests for all edge cases

### Files to Create/Modify

| Action | File | Reason |
|--------|------|--------|
| Modify | `src/teams_phone/services/number_service.py` | Add `import re` and `_extract_operation_id` method |
| Modify | `tests/unit/test_number_service.py` | Add TestNumberServiceExtractOperationId test class |

### Quick Wins
- Start with happy path test (simple URL extraction)
- Then add malformed header test
- Finally add edge cases (query params, full URL)

---

## Cheat Sheet

### Key Imports
```python
# Add to existing imports in number_service.py
import re

# ValidationError should be imported alongside NotFoundError
from teams_phone.exceptions import NotFoundError, ValidationError
```

### Location Header Format Examples
```
# From 2-phone-number-operations-verified.md
https://graph.microsoft.com/beta/admin/teams/telephoneNumberManagement/operations('QXNzaWdubWVudHw2Y2E4Yjc0Ni0...')

# Simplified pattern
.../operations('{operation_id}')
```

### Regex Pattern
```python
# Pattern to extract operation ID from Location header
OPERATION_ID_PATTERN = r"operations\('([^']+)'\)"

match = re.search(OPERATION_ID_PATTERN, location_header)
if match:
    return match.group(1)
```

### Method Signature
```python
def _extract_operation_id(self, location_header: str) -> str:
    """Extract operation ID from Location header.

    Parses the operation ID from the Location header returned by
    202 Accepted responses for assign/unassign operations.

    Args:
        location_header: The Location header value containing the
            operations URL in format: .../operations('{operation_id}')

    Returns:
        The extracted operation ID string.

    Raises:
        ValidationError: If the header format doesn't match the
            expected pattern.
    """
```

### Expected Implementation
```python
def _extract_operation_id(self, location_header: str) -> str:
    """Extract operation ID from Location header."""
    pattern = r"operations\('([^']+)'\)"
    match = re.search(pattern, location_header)

    if not match:
        raise ValidationError(
            f"Invalid Location header format: '{location_header}'",
            remediation=(
                "Expected format: .../operations('{operation_id}'). "
                "This may indicate an API response format change."
            ),
        )

    return match.group(1)
```

### Test Structure
```python
class TestNumberServiceExtractOperationId:
    """Tests for NumberService._extract_operation_id method."""

    @pytest.mark.unit
    def test_extracts_id_from_full_url(self, mock_graph_client: MagicMock) -> None:
        """_extract_operation_id extracts ID from full URL."""
        service = NumberService(mock_graph_client)
        header = "https://graph.microsoft.com/beta/admin/teams/telephoneNumberManagement/operations('op-123-abc')"

        result = service._extract_operation_id(header)

        assert result == "op-123-abc"
```

---

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