# Task Recon: Implement Number Update Command

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

---

## Task Summary

Add an `update` command to the numbers CLI for changing phone number location or network site without requiring reassignment. This is a synchronous operation (unlike assign/unassign which are async) that allows updating emergency location and network site settings on already-assigned phone numbers.

### Dependencies
- Depends on: 19 (NumberService Core Operations - done), 22 (Numbers CLI Read Operations - done), 41 (Validation Checkpoint 3 - done)

### Acceptance Criteria
- `update_number` method in NumberService that makes synchronous POST request to `/updateNumber` endpoint
- CLI command `numbers update <number>` with `--location`, `--clear-location`, `--network-site`, `--clear-network-site` options
- Support for `--dry-run` and `--force` flags
- Phone number formatting uses E.164 format with `+` prefix (via `format_for_update`)
- Unit tests covering all scenarios

---

## Insights from Previous Tasks

### Dependency Task Notes
| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 19 | NumberService Core Operations | City filter is client-side (Graph API limitation); `format_for_update` returns E.164 with + prefix for API calls |
| 22 | Numbers CLI Commands (Read) | Use `model_validate()` with dict when creating Pydantic models in tests; Rich may include ANSI codes |
| 41 | Validation Checkpoint 3 | Location header parsing bug was fixed to handle both OData and REST path formats; policyId runtime resolution pattern established |

### Reusable Patterns from Dependencies
- `_run_async()` helper in `numbers.py:50-78` - pattern for running async operations with NumberService
- `_run_assign_async()` in `numbers.py:97-130` - pattern for commands needing multiple services
- `format_for_update()` in `utils.py:67-88` - already returns E.164 format with + prefix (exactly what update API needs)
- `location_service.validate_location()` in `location_service.py:101-127` - validates location by ID or description

### Warnings and Gotchas
- **E.164 format difference**: `updateNumber` uses `+12065551234` (with +), unlike `assignNumber`/`unassignNumber` which use `12065551234` (digits only)
- **Synchronous operation**: `updateNumber` returns 200 OK immediately, not 202 Accepted - no polling needed
- **Empty string to clear**: Use empty string `""` to clear location or network site; null/omit preserves existing value

### Open Items Inherited
- None that affect this task directly

---

## Architecture Context

### Relevant Design Decisions
- Services abstract Graph API access (repository pattern)
- CLI commands use Typer with async execution helpers
- Pydantic strict mode for API contract validation
- Phone numbers normalized on input using utils functions

### Module Structure
```
src/teams_phone/
├── services/number_service.py  # Add update_number method here
├── cli/numbers.py              # Add update command here
└── utils.py                    # format_for_update already exists
```

### Conventions to Follow
- Use keyword-only arguments for optional params in service methods
- Wrap async operations with `_run_async` or custom helper
- Display dry-run previews with `formatter.info()` calls
- Return TeamsPhoneError exceptions with remediation hints

---

## Related Code References

### Primary References (Must Read)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/services/number_service.py` | 291-418 | `assign_number` and `unassign_number` - patterns for API calls, but note these are async (202 response) while update is sync (200) |
| `src/teams_phone/cli/numbers.py` | 547-757 | `assign_number` CLI command - dry-run, confirmation, resolution patterns |
| `src/teams_phone/utils.py` | 67-88 | `format_for_update` - returns E.164 with + prefix |
| `src/teams_phone/infrastructure/graph_client.py` | 420-457 | `post` method - returns JSON response dict, handles errors |

### Secondary References (Helpful Context)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/services/location_service.py` | 101-127 | `validate_location` - validates location by ID or description |
| `src/teams_phone/cli/numbers.py` | 776-931 | `unassign_number` CLI - simpler command pattern to follow |
| `tests/unit/test_number_service.py` | 1285-1696 | Tests for assign_number - patterns for mocking GraphClient |

### Code Patterns to Follow

```python
# NumberService method pattern for synchronous operation (from graph_client.post)
# src/teams_phone/infrastructure/graph_client.py:420-457
async def post(
    self,
    endpoint: str,
    body: dict[str, Any],
) -> dict[str, Any]:
    """Make an async POST request to a Graph API endpoint."""
    url = self._build_url(endpoint)
    response = await self._request("POST", url, json=body)
    if not response.is_success:
        self._handle_error(response)
    result: dict[str, Any] = response.json()
    return result
```

```python
# CLI confirmation pattern (src/teams_phone/cli/numbers.py:670-681)
if not force and not cli_ctx.json_output:
    formatter.info("Assignment Details:")
    formatter.info(f"  User: {resolved_user.display_name}")
    # ... more details
    confirm = typer.confirm("Proceed with assignment?")
    if not confirm:
        formatter.info("Assignment cancelled")
        raise typer.Exit(0)
```

```python
# Location validation pattern (src/teams_phone/cli/numbers.py:613-615)
if location:
    resolved_location = services.location_service.validate_location(location)
```

---

## Dependencies

### Internal Modules
- `teams_phone.services.number_service.NumberService` - add `update_number` method
- `teams_phone.cli.numbers.numbers_app` - add `update` command
- `teams_phone.utils.format_for_update` - format phone number with + prefix
- `teams_phone.services.location_service.LocationService` - validate location parameter
- `teams_phone.infrastructure.graph_client.GraphClient.post` - synchronous POST request

### External Packages
- `typer` - CLI framework (already in use)
- `httpx` - HTTP client (via GraphClient)
- `pydantic` - model validation (already in use)

### New Dependencies Needed
- None required

---

## Testing Guidance

### Test File Location
`tests/unit/test_number_service.py` - add TestNumberServiceUpdateNumber class
`tests/unit/test_cli_numbers.py` - add TestNumbersUpdateCommand class

### Test Patterns in Use
```python
# Example test structure (tests/unit/test_number_service.py:1290-1339)
@pytest.mark.unit
@pytest.mark.asyncio
async def test_calling_plan_without_location_raises_validation_error(
    self, mock_graph_client: MagicMock
) -> None:
    """assign_number raises ValidationError for CallingPlan without location_id."""
    service = NumberService(mock_graph_client)

    with pytest.raises(ValidationError) as exc_info:
        await service.assign_number(
            user_id="user-123",
            phone_number="+12065551234",
            number_type=NumberType.CALLING_PLAN,
        )

    assert "Emergency location required" in str(exc_info.value)
```

```python
# CLI test pattern (tests/unit/test_cli_numbers.py:94-121)
@pytest.mark.unit
def test_list_shows_numbers(self) -> None:
    """list shows numbers in table format."""
    numbers = [_make_number(telephone_number="+14255551001")]

    with (
        patch("teams_phone.cli.numbers.CacheManager"),
        patch("teams_phone.cli.numbers.AuthService"),
        patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
        patch("teams_phone.cli.numbers.NumberService") as mock_ns_class,
    ):
        # ... mock setup
        result = runner.invoke(app, ["--no-color", "numbers", "list"])
        assert result.exit_code == 0
```

### Fixtures Available
- `mock_graph_client` in `test_number_service.py:93-96` - MagicMock for GraphClient
- `make_number_dict()` in `test_number_service.py:55-91` - creates test number dict

### Test Utilities
- `tests/conftest.py` - pytest markers (unit, integration, contract)
- `typer.testing.CliRunner` - for CLI command testing

---

## Risks and Blockers

### Technical Risks
- [ ] **API Response Format**: The updateNumber API returns 200 OK with empty body per docs. Need to verify actual response structure - may need to use `post_with_response` if we need to check status codes

### Dependency Risks
- None - all dependency tasks are complete

### Open Questions
- [ ] Should `--clear-location` be mutually exclusive with `--location`? (Spec implies yes)
- [ ] What happens if updating location on an unassigned number? (Need to validate number is assigned first)
- [ ] Should the command require the number to be assigned before updating? (CLI spec says "Number must be assigned to a user")

---

## Implementation Recommendations

### Suggested Approach
1. Add `update_number` method to `NumberService` in `src/teams_phone/services/number_service.py`
2. Add `update` command to `numbers_app` in `src/teams_phone/cli/numbers.py`
3. Write unit tests for service method in `tests/unit/test_number_service.py`
4. Write CLI tests in `tests/unit/test_cli_numbers.py`
5. Run full test suite to verify no regressions

### Files to Create/Modify
| Action | File | Reason |
|--------|------|--------|
| Modify | `src/teams_phone/services/number_service.py` | Add `update_number()` method |
| Modify | `src/teams_phone/cli/numbers.py` | Add `update` command |
| Modify | `tests/unit/test_number_service.py` | Add `TestNumberServiceUpdateNumber` class |
| Modify | `tests/unit/test_cli_numbers.py` | Add `TestNumbersUpdateCommand` class |

### Quick Wins
- Start with the `update_number` service method since it's simpler (sync, no polling)
- The CLI command can reuse existing patterns from `unassign` command (simpler than assign)

---

## Cheat Sheet

### Key Imports
```python
# For NumberService
from teams_phone.utils import format_for_update
from teams_phone.exceptions import ValidationError

# For CLI
from teams_phone.services.location_service import LocationService
from teams_phone.cli.context import get_context
```

### Key Functions to Use
- `format_for_update(number)` - returns `+12065551234` format for updateNumber API
- `graph_client.post(endpoint, body)` - synchronous POST, returns dict (or empty for 200 no-content)
- `location_service.validate_location(identifier)` - returns EmergencyLocation or raises NotFoundError

### API Endpoint Details
```
POST /admin/teams/telephoneNumberManagement/numberAssignments/updateNumber
Request Body:
{
  "telephoneNumber": "+12061234567",  // E.164 with + prefix
  "locationId": "uuid-here",           // Optional - omit to preserve, "" to clear
  "networkSiteId": "site-id"           // Optional - omit to preserve, "" to clear
}
Response: 200 OK (empty body)
```

### Common Patterns
- Dry-run preview: See `numbers.py:642-667` (assign command)
- Confirmation prompt: See `numbers.py:669-681` (assign command)
- Location validation: See `numbers.py:613-634` (assign command)
- Simple _run_async: See `numbers.py:50-78` - can be used since no LocationService needed for basic update

---

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