# Task Recon: Implement CSV Parsing and Validation Phase with Rich Summary Display

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

---

## Task Summary

Create the `bulk-assign` CLI command function with CSV file parsing, row validation using Task 26 utilities, and Rich summary display showing validation results before proceeding. This is the first subtask of implementing the Numbers Bulk-Assign Command.

### Parent Task Context

**Parent Task:** Implement Numbers Bulk-Assign Command (ID: 27)
**Parent Goal:** Add bulk-assign command to numbers CLI with progress bar, continue-on-error, and results file output. This enables batch phone number assignments from a CSV file with validation, progress tracking, and error handling.
**This Subtask's Role:** Sets up the command structure, handles CSV parsing/validation phase, and displays the validation summary. Later subtasks (27.2, 27.3) will handle the execution phase with progress bar and error handling/results output.

### Dependencies
- Task 21: Implement Number Assign/Unassign Operations (done) - Provides `NumberService.assign_number()` method
- Task 23: Implement Numbers Assign/Unassign CLI Commands (done) - Provides patterns for async service calls, progress display, confirmation prompts
- Task 26: Implement Bulk CSV Parsing and Validation (done) - Provides `parse_assign_csv()`, `BulkOperations.validate_assign_csv()`, `write_results_csv()`
- Task 41: Validation Checkpoint 3 (done) - Write operations validated

### Acceptance Criteria
From task details:
1. Add `bulk_assign` command with all CLI options (csv_file, --dry-run, --continue-on-error, --default-location, --output, --yes)
2. Create async helper similar to `_run_assign_async` for multi-service operations
3. Parse CSV using `parse_assign_csv()` from Task 26
4. Validate all rows using `validate_assign_csv()` async method
5. Display validation summary using OutputFormatter (table with status colors, counts)
6. If --dry-run, exit after validation summary with appropriate exit code
7. If validation errors exist and not --continue-on-error, exit with ValidationError
8. Unless --yes, prompt for confirmation before proceeding

---

## Insights from Previous Tasks

### Dependency Task Notes
| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 21 | Number Assign/Unassign Operations | `assign_number()` accepts `on_progress` callback for progress updates; uses `format_for_assign()` for phone number normalization |
| 23 | Numbers Assign/Unassign CLI | `_run_assign_async` helper for multi-service operations; `AssignServices` class bundles services; confirmation prompt pattern with `typer.confirm()` |
| 26 | Bulk CSV Parsing and Validation | `parse_assign_csv()` is standalone function (not method); `BulkOperations` class has `validate_assign_csv()` async method; row numbers are 1-indexed starting at 2 |
| 41 | Validation Checkpoint 3 | Location header parsing fixed to support both OData and REST formats; Windows Unicode encoding handled with ASCII fallback |

### Reusable Patterns from Dependencies
- `AssignServices` class pattern: Implemented in `src/teams_phone/cli/numbers.py:72-86` - bundles UserService, NumberService, LocationService, ConfigManager
- `_run_assign_async` helper: Implemented in `src/teams_phone/cli/numbers.py:88-121` - creates services and runs async coro
- Confirmation prompt: `typer.confirm()` usage at `numbers.py:634` - shows details before prompting
- JSON mode skip confirmation: Check `cli_ctx.json_output` to skip prompts in JSON mode

### Warnings and Gotchas
- **Circular import warning from Task 26**: Run tests via `pytest tests/unit/` not individual test files
- **LocationService is sync**: `validate_location()` and `is_cache_stale()` are synchronous methods
- **Row numbers start at 2**: First data row is row 2 (row 1 is header)
- **CSV DictReader returns strings**: All values are strings, not typed

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

---

## Architecture Context

### Relevant Design Decisions
- **Layered Monolith**: CLI layer calls services, services call GraphClient/CacheManager
- **Sequential bulk operations**: Per bulk-operations-implementation-strategy.md, bulk-assign uses sequential processing for better progress reporting and error handling (not batch API)
- **BulkOperations class**: Task 26 created `BulkOperations` class that takes services via dependency injection

### Module Structure
```
src/teams_phone/
├── cli/numbers.py           # Add bulk_assign command here
├── services/bulk_operations.py  # BulkOperations class, parse_assign_csv(), write_results_csv()
```

### Conventions to Follow
- Use `@numbers_app.command("bulk-assign")` decorator with hyphenated name
- Create async helper function for service initialization pattern
- Use `OutputFormatter` for all display output
- Handle `TeamsPhoneError` exceptions and format via `formatter.error()`
- Exit with appropriate exit codes from `constants.py` or exception.exit_code

---

## Related Code References

### Primary References (Must Read)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/cli/numbers.py` | 503-713 | `assign_number` command - shows full pattern for CLI command with async services, dry-run, confirmation, progress |
| `src/teams_phone/cli/numbers.py` | 72-121 | `AssignServices` class and `_run_assign_async` helper - pattern to replicate for bulk operations |
| `src/teams_phone/services/bulk_operations.py` | 80-177 | `parse_assign_csv()` function - parse CSV to BulkAssignRow list |
| `src/teams_phone/services/bulk_operations.py` | 239-347 | `BulkOperations.validate_assign_csv()` - async validation method |

### Secondary References (Helpful Context)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/infrastructure/output_formatter.py` | 87-113 | `table()` method - display tabular data |
| `src/teams_phone/infrastructure/output_formatter.py` | 191-227 | `progress()` context manager - for later subtasks |
| `src/teams_phone/cli/numbers.py` | 166-197 | Format helper functions for status display |
| `.taskmaster/docs/cli-specification/command-group-2-numbers.md` | 344-402 | CLI spec for bulk-assign command |

### Code Patterns to Follow

```python
# Pattern from numbers.py:503-555 - Command signature with options
@numbers_app.command("bulk-assign")
def bulk_assign(
    ctx: typer.Context,
    csv_file: Path = typer.Argument(..., help="CSV file with user, phone_number, location columns"),
    dry_run: bool = typer.Option(False, "--dry-run", help="Validate only, no changes"),
    continue_on_error: bool = typer.Option(False, "--continue-on-error", help="Continue after failures"),
    default_location: Optional[str] = typer.Option(None, "--default-location"),
    output: Optional[Path] = typer.Option(None, "--output", "-o", help="Results file path"),
    yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
) -> None:
    """Bulk assign phone numbers from CSV file."""
    ...
```

```python
# Pattern from numbers.py:88-121 - Multi-service async helper
def _run_bulk_assign_async(
    ctx: typer.Context,
    coro_factory: Any,
) -> tuple[Any, "OutputFormatter"]:
    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:
            # Create services and BulkOperations
            user_service = UserService(graph_client)
            number_service = NumberService(graph_client)
            location_service = LocationService(cache_manager)
            bulk_ops = BulkOperations(user_service, number_service, location_service)
            return await coro_factory(bulk_ops, config_manager)

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

```python
# Pattern from numbers.py:625-637 - Confirmation prompt
if not yes and not cli_ctx.json_output:
    formatter.info("Assignment Details:")
    formatter.info(f"  Total rows: {len(rows)}")
    formatter.info(f"  Valid: {result.valid_count}")
    formatter.info(f"  Errors: {result.error_count}")
    formatter.info("")
    confirm = typer.confirm("Proceed with bulk assignment?")
    if not confirm:
        formatter.info("Bulk assignment cancelled")
        raise typer.Exit(0)
```

---

## Dependencies

### Internal Modules
- `teams_phone.cli.context` - `get_context()` function for CLIContext
- `teams_phone.cli.numbers` - Add command here, reuse AssignServices pattern
- `teams_phone.services.bulk_operations` - `parse_assign_csv`, `BulkOperations`, `BulkValidationResult`
- `teams_phone.infrastructure` - `CacheManager`, `ConfigManager`, `GraphClient`, `OutputFormatter`
- `teams_phone.services` - `AuthService`, `UserService`, `NumberService`, `LocationService`
- `teams_phone.exceptions` - `TeamsPhoneError`, `ValidationError`

### External Packages
- `typer` - CLI framework (already in use)
- `pathlib.Path` - File path handling (already in use)

### New Dependencies Needed
- None required - all necessary modules already imported or available

---

## Testing Guidance

### Test File Location
`tests/unit/test_cli_numbers.py` - Add new test class `TestNumbersBulkAssignCommand`

### Test Patterns in Use
```python
# Example test pattern from test_cli_numbers.py:732-777
@pytest.mark.unit
def test_assign_dry_run_shows_preview(self) -> None:
    """assign --dry-run shows preview without executing."""
    mock_user = _make_user()
    mock_location = _make_location()

    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.UserService") as mock_us_class,
        patch("teams_phone.cli.numbers.NumberService"),
        patch("teams_phone.cli.numbers.LocationService") as mock_ls_class,
    ):
        # Setup mocks...
        result = runner.invoke(app, ["--no-color", "numbers", "bulk-assign", "test.csv", "--dry-run"])
        assert result.exit_code == 0
        assert "Dry Run" in result.stdout or "Validation" in result.stdout
```

### Fixtures Available
- `_make_number()` - Create NumberAssignment for testing (test_cli_numbers.py:34-58)
- `_make_user()` - Create UserConfiguration for testing (test_cli_numbers.py:660-676)
- `_make_location()` - Create EmergencyLocation for testing (test_cli_numbers.py:679-689)
- `_make_operation()` - Create AsyncOperation for testing (test_cli_numbers.py:692-702)

### Test Utilities
- `tests/conftest.py` - pytest fixtures
- `typer.testing.CliRunner` - CLI test runner
- `unittest.mock.patch` - For mocking services

---

## Risks and Blockers

### Technical Risks
- [ ] **Validation summary display format**: Need to decide exact Rich table format for validation results. Mitigate by following existing table() pattern in OutputFormatter.
- [ ] **Exit code handling**: Dry-run with errors should exit with code 1 (validation errors) vs 0 (success). Document behavior clearly.

### Dependency Risks
- [ ] None - all dependencies (Tasks 21, 23, 26, 41) are marked done

### Open Questions
- [ ] Should validation summary table show all rows or just summary counts? CLI spec says "Live progress bar showing completion percentage" for execution but validation display is less specified.
- [ ] What happens if CSV file is empty (header only)? Task 26 returns empty list - should show "No rows to process" and exit 0?

---

## Implementation Recommendations

### Suggested Approach
1. Add imports for `BulkOperations`, `parse_assign_csv`, `BulkValidationResult` from `bulk_operations`
2. Create `_run_bulk_assign_async` helper function following `_run_assign_async` pattern
3. Implement `bulk_assign` command function with all CLI options
4. Add CSV parsing with error handling for missing file
5. Add async validation call via helper
6. Implement validation summary display (table or formatted info)
7. Handle dry-run exit
8. Handle validation error exit (without --continue-on-error)
9. Add confirmation prompt
10. Add placeholder for execution phase (to be completed in subtask 27.2)

### Files to Create/Modify
| Action | File | Reason |
|--------|------|--------|
| Modify | `src/teams_phone/cli/numbers.py` | Add bulk_assign command, _run_bulk_assign_async helper |
| Create | `tests/unit/test_cli_numbers_bulk.py` (optional) or add to `test_cli_numbers.py` | Unit tests for bulk-assign command |

### Quick Wins
- Start with command signature and basic CSV parsing (no validation)
- Verify command appears in `numbers --help`
- Add dry-run that just shows parsed row count

---

## Cheat Sheet

### Key Imports
```python
from pathlib import Path
from typing import Optional

from teams_phone.services.bulk_operations import (
    BulkOperations,
    BulkValidationResult,
    parse_assign_csv,
)
```

### Key Functions to Use
- `parse_assign_csv(file_path: Path) -> list[BulkAssignRow]` - Parse CSV file
- `BulkOperations.validate_assign_csv(rows) -> BulkValidationResult` - Async validation
- `formatter.table(data, columns, title)` - Display tabular data
- `formatter.info(message)` - Display info message
- `typer.confirm(message)` - Confirmation prompt

### Common Patterns
- Multi-service async: See `_run_assign_async` in `numbers.py:88-121`
- Confirmation prompt: See `numbers.py:625-637`
- Dry-run handling: See `numbers.py:597-623`
- Error handling: Catch `TeamsPhoneError` and use `formatter.error(e.message, remediation=e.remediation)`

### Validation Summary Display Pattern
```python
# Display validation results as a table
if not cli_ctx.json_output:
    # Summary counts
    formatter.info(f"Validation Summary:")
    formatter.info(f"  Total rows: {len(validation_result.rows)}")
    formatter.info(f"  Valid: {validation_result.valid_count}")
    formatter.info(f"  Errors: {validation_result.error_count}")
    formatter.info(f"  Warnings: {validation_result.warning_count}")

    # Optionally show rows with errors/warnings in table
    if validation_result.error_count > 0:
        error_rows = [
            {"row": r.row_number, "user": r.user, "error": "; ".join(r.errors)}
            for r in validation_result.rows if r.errors
        ]
        formatter.table(error_rows, ["row", "user", "error"], title="Validation Errors")
```

### Exit Codes
- 0: Success (dry-run with no errors, cancelled by user)
- 1: General failure (operation failed)
- 5: ValidationError (validation errors without --continue-on-error)

---

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