# Task Recon: Implement Phone Number Utilities

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

---

## Task Summary

Create utility functions in `src/teams_phone/utils.py` for phone number normalization, validation, and format conversion between different Graph API endpoint requirements. The file exists as a stub with a single docstring.

**Key Functions to Implement:**
1. `normalize_phone_number(number: str) -> str` - Strip non-digits, validate minimum length
2. `format_for_assign(number: str) -> str` - Format for assignNumber/unassignNumber endpoints (digits only)
3. `format_for_update(number: str) -> str` - Format for updateNumber endpoint (E.164 with + prefix)
4. `format_for_display(number: str) -> str` - Format for user display (+1 (206) 555-1234)
5. `match_pattern(number: str, pattern: str) -> bool` - Wildcard pattern matching for search

### Dependencies
- Depends on: Task 1 (Initialize Python Project Structure) - DONE
- Depends on: Task 2 (Constants and Exception Hierarchy) - DONE

### Acceptance Criteria
From task details and CLI specification:
- Input accepts various formats: E.164 (+12065551234), with spaces, dashes, parentheses
- Output formats depend on target API endpoint:
  - assignNumber/unassignNumber: digits only (no `+`)
  - updateNumber: E.164 with `+` prefix
  - User display: +1 (206) 555-1234 format
- ValidationError raised for invalid numbers (less than 10 digits after normalization)
- Pattern matching supports wildcards: `206*`, `*555*`, partial digit matches

---

## Insights from Previous Tasks

### Dependency Task Notes
| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 1 | Initialize Python Project Structure | mypy strict mode requires complete type hints on all functions |
| 2 | Constants and Exception Hierarchy | ValidationError is ready to use with exit_code=5 |

### Reusable Patterns from Dependencies
- **Exception import pattern**: Import from `teams_phone.exceptions` (see `exceptions.py:85-95`)
- **Docstring style**: Google-style docstrings established in the codebase

### Warnings and Gotchas
- **Mypy strict mode**: All functions need complete type hints - mypy will fail on untyped code
- **ValidationError already exists**: Do NOT create a new one; import from `teams_phone.exceptions`
- **utils.py is a stub**: File exists with only a docstring - must implement, not create fresh

### Open Items Inherited
- None directly applicable to this task

---

## Architecture Context

### Relevant Design Decisions
- **CLAUDE.md**: utils.py is designated for "Phone number normalization" in the architecture
- **Graph API Contracts**: Different endpoints require different phone number formats:
  - `assignNumber`: digits only (no `+` prefix) e.g., `12061234567`
  - `unassignNumber`: digits only (no `+` prefix) e.g., `12061234567`
  - `updateNumber`: E.164 with `+` prefix e.g., `+12061234567`

### Module Structure
```
src/teams_phone/
├── utils.py          # <-- This task (phone number utilities)
├── exceptions.py     # ValidationError defined here
├── constants.py      # ExitCode enum (used by exceptions)
└── models/
    └── number.py     # NumberAssignment model (reference for E.164 format)
```

### Conventions to Follow
- Type hints on all functions (strict mypy)
- Google-style docstrings
- Import ValidationError from teams_phone.exceptions
- Use `re` module for regex operations (standard library)

---

## Related Code References

### Primary References (Must Read)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/utils.py` | 1-2 | Current stub file to implement |
| `src/teams_phone/exceptions.py` | 85-95 | ValidationError class to import |
| `.taskmaster/docs/graph-api-contracts/2-phone-number-operations-verified.md` | 93-101 | assignNumber format (digits only) |
| `.taskmaster/docs/graph-api-contracts/2-phone-number-operations-verified.md` | 199-205 | updateNumber format (E.164 with +) |

### Secondary References (Helpful Context)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/models/number.py` | 132-186 | NumberAssignment model (telephoneNumber field is E.164) |
| `.taskmaster/docs/cli-specification/command-group-2-numbers.md` | 165-168 | CLI phone number format normalization spec |
| `tests/unit/test_exceptions.py` | 90-95 | How ValidationError is tested |

### Code Patterns to Follow

```python
# Exception usage pattern (src/teams_phone/exceptions.py:85-95)
class ValidationError(TeamsPhoneError):
    """Input validation failure (exit code 5).

    Raised when input data fails validation:
    - Invalid phone number format
    ...
    """

    exit_code: int = ExitCode.VALIDATION_ERROR
```

```python
# Test class pattern (tests/unit/test_exceptions.py:66-116)
class TestExitCodeMapping:
    """Tests verifying each exception maps to correct exit code."""

    @pytest.mark.unit
    def test_validation_error_exit_code(self) -> None:
        """ValidationError maps to VALIDATION_ERROR (5)."""
        exc = ValidationError("Invalid phone number")
        assert exc.exit_code == ExitCode.VALIDATION_ERROR
        assert exc.exit_code == 5
```

---

## Dependencies

### Internal Modules
- `teams_phone.exceptions.ValidationError` - For raising validation errors on invalid numbers

### External Packages
- `re` (standard library) - For regex pattern matching and digit extraction

### New Dependencies Needed
- None required - `re` is in Python standard library

---

## Testing Guidance

### Test File Location
`tests/unit/test_utils.py` (to be created)

### Test Patterns in Use
```python
# Test class organization pattern (tests/unit/test_exceptions.py)
class TestClassName:
    """Tests for specific functionality."""

    @pytest.mark.unit
    def test_specific_behavior(self) -> None:
        """Clear description of what is tested."""
        # Arrange
        ...
        # Act
        ...
        # Assert
        assert result == expected
```

```python
# Testing exceptions are raised (tests/unit/test_exceptions.py:206-210)
@pytest.mark.unit
def test_raise_and_catch_base(self) -> None:
    """Base exception can be raised and caught."""
    with pytest.raises(TeamsPhoneError) as exc_info:
        raise TeamsPhoneError("Test error")
    assert str(exc_info.value) == "Test error"
```

### Fixtures Available
- None specific to utils - tests should be self-contained

### Test Utilities
- `tests/conftest.py` - Marker registration (unit, integration, contract)

### Test Cases to Cover (from task description)
1. **normalize_phone_number:**
   - Input with `+` prefix: `+12065551234` -> `12065551234`
   - Input with spaces: `1 206 555 1234` -> `12065551234`
   - Input with dashes: `1-206-555-1234` -> `12065551234`
   - Input with parentheses: `+1 (206) 555-1234` -> `12065551234`
   - Invalid: less than 10 digits -> raises ValidationError
   - Invalid: empty string -> raises ValidationError
   - Invalid: only non-digits -> raises ValidationError

2. **format_for_assign:**
   - Same as normalize (digits only)

3. **format_for_update:**
   - Returns with `+` prefix: `12065551234` -> `+12065551234`
   - Input already has `+`: `+12065551234` -> `+12065551234`

4. **format_for_display:**
   - US number: `12065551234` -> `+1 (206) 555-1234`
   - International (varies by country code length)

5. **match_pattern:**
   - Last N digits: `1234` matches `+12065551234`
   - Area code wildcard: `206*` matches all 206 numbers
   - Full number exact match: `+12065551234`
   - Contains pattern: `*555*` matches numbers with 555

---

## Risks and Blockers

### Technical Risks
- [ ] **International number handling**: Task description focuses on US format. Need to determine if international numbers (different country code lengths, different digit counts) need special handling. Recommendation: Start with US-centric (10-11 digits minimum) and document as limitation.

### Dependency Risks
- None - all dependencies are complete

### Open Questions
- [ ] Should `format_for_display` handle international numbers differently (e.g., UK +44)?
- [ ] What is the minimum phone number length? Task says 10 digits - is this after stripping country code or total?
- [ ] Should `match_pattern` normalize both inputs before matching?

---

## Implementation Recommendations

### Suggested Approach
1. Start with `normalize_phone_number` - core function others depend on
2. Implement `format_for_assign` (trivially wraps normalize)
3. Implement `format_for_update` (normalize + add `+` prefix)
4. Implement `format_for_display` (US NANP format: +1 (NPA) NXX-XXXX)
5. Implement `match_pattern` (convert wildcards to regex, match against normalized number)
6. Create comprehensive unit tests in `tests/unit/test_utils.py`

### Files to Create/Modify
| Action | File | Reason |
|--------|------|--------|
| Modify | `src/teams_phone/utils.py` | Implement all utility functions |
| Create | `tests/unit/test_utils.py` | Unit tests for all functions |

### Quick Wins
- `format_for_assign` is trivial once `normalize_phone_number` works
- Use `re.sub(r'\D', '', number)` for stripping non-digits (shown in task details)

---

## Cheat Sheet

### Key Imports
```python
import re
from teams_phone.exceptions import ValidationError
```

### Key Functions to Use
- `re.sub(r'\D', '', number)` - Strip all non-digit characters
- `re.fullmatch(pattern, string)` - Full string regex match
- `re.escape(pattern)` - Escape special regex characters

### Common Patterns
- **Digit extraction**: `digits = re.sub(r'\D', '', number)`
- **Length validation**: `if len(digits) < 10: raise ValidationError(...)`
- **E.164 format**: `f"+{digits}"`
- **Wildcard to regex**: Replace `*` with `.*` for pattern matching

### Graph API Format Reference
| Endpoint | Format Example | Notes |
|----------|----------------|-------|
| assignNumber | `12061234567` | Digits only, no `+` |
| unassignNumber | `12061234567` | Digits only, no `+` |
| updateNumber | `+12061234567` | E.164 with `+` prefix |
| List response telephoneNumber | `+12052582895` | E.164 with `+` prefix |

---

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