# Task Recon: Implement Numbers CLI Commands (Read Operations)

**Task ID:** 22
**Status:** pending
**Generated:** 2026-01-29T16:00:00Z
**Agent:** claude-opus-4-5-20251101

---

## Task Summary

Create the `numbers` command group with `list`, `show`, and `search` commands for phone number inventory management. This is the CLI layer that wraps the already-implemented NumberService (Task 19).

### Dependencies
- Depends on: Task 8 (OutputFormatter - done), Task 11 (Root CLI App - done), Task 19 (NumberService - done)

### Acceptance Criteria
1. `numbers list` command with filters: `--status`, `--type`, `--city`, `--limit`, `--all`
2. `numbers show <number>` command to display detailed phone number information
3. `numbers search <pattern>` command with wildcard pattern support and `--limit` option
4. JSON output mode support via global `--json` flag
5. Error handling with appropriate exit codes
6. CLI tests with mocked NumberService

---

## Insights from Previous Tasks

### Dependency Task Notes

| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 8 | OutputFormatter | JSON mode outputs via plain `print()` to avoid ANSI codes; error remediation uses "To fix:" prefix; `NO_COLOR` env var respected |
| 11 | Root CLI App | CLIContext dataclass replaces raw dict; `get_context()` for type-safe retrieval; `no_args_is_help=True` returns exit code 2; OutputFormatter initialized eagerly, ConfigManager lazy |
| 19 | NumberService | `list_numbers()` supports status/number_type filters via OData, city is client-side; `get_number()` uses `format_for_update()` for normalization; `search_numbers()` fetches all then applies `match_pattern()` client-side |

### Reusable Patterns from Dependencies
- **Async execution pattern**: See `users.py:40-68` - `_run_async()` helper creates service and runs coroutine
- **Model to dict conversion**: See `users.py:100-140` - `_user_to_list_dict()` and `_user_to_json_dict()` patterns
- **Error handling**: See `users.py:231-233` - Catch `TeamsPhoneError`, display with formatter, exit with code
- **Filter validation**: See `users.py:186-192` - Check mutually exclusive `--limit` and `--all`

### Warnings and Gotchas
- **Warning from Task 8**: Rich interprets `...` patterns - avoid in test strings
- **Warning from Task 11**: Exit code 2 for `no_args_is_help=True` - tests must account for this
- **Warning from Task 19**: City filter is client-side (performance impact for large inventories); `search_numbers` fetches ALL numbers before filtering

### Open Items Inherited
- None - all dependency tasks are complete

---

## Architecture Context

### Relevant Design Decisions
- **Layered architecture**: CLI layer (`cli/`) wraps Services layer (`services/`) which wraps Infrastructure (`infrastructure/`)
- **Async service pattern**: Services are async, CLI commands use `asyncio.run()` via helper function
- **Error exit codes**: Each exception type maps to specific exit code (2-8) per `exceptions.py`
- **JSON mode**: Global `--json` flag outputs pure JSON to stdout, bypassing Rich formatting

### Module Structure
```
src/teams_phone/
├── cli/
│   ├── numbers.py          # NEW FILE - to be created
│   ├── main.py             # Register numbers_app here (already has placeholder)
│   ├── users.py            # Primary reference pattern
│   └── locations.py        # Secondary reference pattern
├── services/
│   └── number_service.py   # Service layer (already done)
└── models/
    └── number.py           # NumberAssignment, AssignmentStatus, NumberType enums
```

### Conventions to Follow
- Use `from __future__ import annotations` for forward references
- Use `typing.TYPE_CHECKING` for import-time-only imports
- Create Typer app with `no_args_is_help=True`
- Use `get_context(ctx)` for type-safe CLIContext access
- Match help text style: Imperative sentences ending with period
- Apply `@pytest.mark.unit` to all unit tests

---

## Related Code References

### Primary References (Must Read)

| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/cli/users.py` | 1-372 | Complete reference implementation for CLI commands (list/show/search pattern) |
| `src/teams_phone/cli/main.py` | 76-80 | Existing `numbers_app` placeholder that must be replaced with import |
| `src/teams_phone/services/number_service.py` | 48-196 | NumberService methods to call (list_numbers, get_number, search_numbers) |
| `src/teams_phone/models/number.py` | 14-26, 46-59, 132-185 | NumberType, AssignmentStatus enums, NumberAssignment model |

### Secondary References (Helpful Context)

| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/cli/context.py` | 76-93 | `get_context()` helper and CLIContext type |
| `src/teams_phone/cli/locations.py` | 84-156 | Alternative pattern for simpler (non-async) CLI commands |
| `tests/unit/test_cli_users.py` | 70-78, 93-111 | Test mock patterns for async service calls |
| `.taskmaster/docs/cli-specification/command-group-2-numbers.md` | 1-122 | Full CLI specification for numbers commands |

### Code Patterns to Follow

```python
# Example async execution pattern from users.py:40-68
def _run_async(
    ctx: typer.Context,
    coro_factory: Any,
) -> tuple[Any, "OutputFormatter"]:
    """Run an async operation with NumberService."""
    cli_ctx = get_context(ctx)
    formatter = cli_ctx.get_output_formatter()
    config_manager = cli_ctx.get_config_manager()
    cache_manager = CacheManager()
    auth_service = AuthService(config_manager, cache_manager)

    async def _execute() -> Any:
        async with GraphClient(auth_service) as graph_client:
            service = NumberService(graph_client)
            return await coro_factory(service)

    result = asyncio.run(_execute())
    return result, formatter
```

```python
# Example command pattern from users.py:151-233
@numbers_app.command("list")
def list_numbers(
    ctx: typer.Context,
    status: Optional[AssignmentStatus] = typer.Option(None, "--status", help="..."),
    # ... other options
) -> None:
    """List phone numbers in inventory."""
    cli_ctx = get_context(ctx)
    formatter = cli_ctx.get_output_formatter()

    try:
        numbers, _ = _run_async(
            ctx,
            lambda service: service.list_numbers(status=status, ...),
        )

        if cli_ctx.json_output:
            formatter.json([_number_to_json_dict(n) for n in numbers])
        else:
            # Table output
            ...
    except TeamsPhoneError as e:
        formatter.error(e.message, remediation=e.remediation)
        raise typer.Exit(e.exit_code)
```

---

## Dependencies

### Internal Modules
- `teams_phone.cli.context` - `CLIContext`, `get_context()`
- `teams_phone.exceptions` - `TeamsPhoneError`, `NotFoundError`
- `teams_phone.infrastructure` - `CacheManager`, `GraphClient`, `OutputFormatter`
- `teams_phone.models` - `NumberAssignment`, `AssignmentStatus`, `NumberType`, `ActivationState`
- `teams_phone.services` - `AuthService`
- `teams_phone.services.number_service` - `NumberService` (direct import to avoid circular)

### External Packages
- `typer` - CLI framework (already in use)
- `asyncio` - For running async service methods

### New Dependencies Needed
- None required

---

## Testing Guidance

### Test File Location
`tests/unit/test_cli_numbers.py` (new file)

### Test Patterns in Use
```python
# Example test structure from tests/unit/test_cli_users.py:93-110
@pytest.mark.unit
def test_list_empty(self) -> None:
    """list shows empty message when no numbers found."""
    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_gc = AsyncMock()
        mock_gc.__aenter__.return_value = mock_gc
        mock_gc.__aexit__.return_value = None
        mock_gc_class.return_value = mock_gc

        mock_service = MagicMock()
        mock_service.list_numbers = AsyncMock(return_value=[])
        mock_ns_class.return_value = mock_service

        result = runner.invoke(app, ["numbers", "list"])
        assert result.exit_code == 0
        assert "No numbers found" in result.stdout
```

### Fixtures Available
- `CliRunner` from `typer.testing`
- Custom fixture needed: `_make_number()` helper (similar to `_make_user()` in test_cli_users.py)

### Test Utilities
- `tests/conftest.py` - Custom markers: `unit`, `integration`, `contract`
- `tests/unit/test_number_service.py:55-90` - `make_number_dict()` helper can be reused for consistency

---

## Risks and Blockers

### Technical Risks
- [x] **Performance for large inventories**: `search` command fetches all numbers client-side. Document in help text.
- [x] **Filter value validation**: CLI spec shows `--status assigned/unassigned` but model has `AssignmentStatus.USER_ASSIGNED`, etc. Need mapping.

### Dependency Risks
- None - all dependencies (Tasks 8, 11, 19) are complete

### Open Questions
- [ ] Should `--status` option accept simplified values (`assigned`/`unassigned`) or exact enum values (`userAssigned`/`unassigned`)? CLI spec shows `assigned/unassigned/reserved` but API uses `userAssigned`, `conferenceAssigned`, `voiceApplicationAssigned`, `unassigned`. **Recommendation**: Create a CLI-friendly enum or accept both.
- [ ] Should `--type` accept kebab-case (`calling-plan`) or exact API values (`callingPlan`)? CLI spec shows kebab-case. **Recommendation**: Accept kebab-case and convert.

---

## Implementation Recommendations

### Suggested Approach
1. **Create `src/teams_phone/cli/numbers.py`** with the `numbers_app` Typer instance
2. **Update `src/teams_phone/cli/main.py`** to import `numbers_app` from new module (replace placeholder)
3. **Implement `_run_async()` helper** following users.py pattern
4. **Implement conversion helpers**: `_number_to_list_dict()`, `_number_to_json_dict()`
5. **Implement `list` command** with all filters (status, type, city, limit, all)
6. **Implement `show` command** with detailed output sections per CLI spec
7. **Implement `search` command** with pattern argument and limit option
8. **Create test file** `tests/unit/test_cli_numbers.py` with mocked service tests

### Files to Create/Modify

| Action | File | Reason |
|--------|------|--------|
| Create | `src/teams_phone/cli/numbers.py` | Main implementation of numbers commands |
| Modify | `src/teams_phone/cli/main.py` | Replace placeholder `numbers_app` with import from new module |
| Create | `tests/unit/test_cli_numbers.py` | Unit tests for CLI commands |

### Quick Wins
1. Start with `list` command - it's the simplest and establishes the async pattern
2. Copy `_run_async()` from users.py and adapt for NumberService
3. Use `make_number_dict()` from test_number_service.py for test fixtures

---

## Cheat Sheet

### Key Imports
```python
from __future__ import annotations

import asyncio
from typing import TYPE_CHECKING, Any, Optional

import typer

from teams_phone.cli.context import get_context
from teams_phone.exceptions import TeamsPhoneError
from teams_phone.infrastructure import CacheManager, GraphClient
from teams_phone.models import AssignmentStatus, NumberAssignment, NumberType
from teams_phone.services import AuthService
from teams_phone.services.number_service import NumberService  # Direct import

if TYPE_CHECKING:
    from teams_phone.infrastructure import OutputFormatter
```

### Key Functions to Use
- `get_context(ctx: typer.Context) -> CLIContext` - Get typed context
- `cli_ctx.get_output_formatter() -> OutputFormatter` - Get formatter
- `cli_ctx.get_config_manager() -> ConfigManager` - Get config (lazy)
- `NumberService.list_numbers(status=, number_type=, city=, max_items=)` - List with filters
- `NumberService.get_number(phone_number)` - Get single number
- `NumberService.search_numbers(pattern, max_results=)` - Search by pattern

### Common Patterns
- **Async execution**: See `users.py:40-68` for `_run_async()` pattern
- **Table columns for list**: `telephone_number`, `assignment_status`, `number_type`, `city`, `assigned_to`
- **Error handling**: `except TeamsPhoneError as e: formatter.error(...); raise typer.Exit(e.exit_code)`

### NumberAssignment Fields for Display
- `telephone_number` - Phone number in E.164 format
- `assignment_status` - AssignmentStatus enum value
- `number_type` - NumberType enum value
- `city` - City (optional)
- `assignment_target_id` - User ID if assigned (optional)
- `activation_state` - ActivationState enum value
- `location_id` - Emergency location ID (optional)
- `capabilities` - List of capability strings
- `port_in_status` - Port status (optional)
- `iso_country_code` - Country code (optional)

---

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