# Task ID: 27

**Title:** Implement Numbers Bulk-Assign Command

**Status:** pending

**Dependencies:** 21, 23, 26

**Priority:** medium

**Description:** Add bulk-assign command to numbers CLI with progress bar, continue-on-error, and results file output.

**Details:**

Extend `src/teams_phone/cli/numbers.py`:
```python
@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"),
):
    """Bulk assign phone numbers from CSV file."""
    # 1. Parse CSV
    # 2. Validate all rows (show summary)
    # 3. If dry-run, exit after validation
    # 4. Confirm unless --yes
    # 5. Process with progress bar
    # 6. Write results file
    # 7. Show summary
    ...
```

Progress bar shows: current/total, success/fail counts.
Default output: bulk-assign-results-{timestamp}.csv

**Test Strategy:**

CLI tests: dry-run validation, progress updates, continue-on-error behavior, results file content, confirmation prompt, default location fallback.

---

## Implementation Notes

### Session: 2026-01-29 - Agent: claude-sonnet-4-5-20250929

**Status:** Subtask 27.1 Implemented

#### What Was Done (Subtask 27.1: CSV Parsing and Validation Phase)
- Added imports for `BulkOperations`, `BulkValidationResult`, `parse_assign_csv` from `bulk_operations` module
- Created `_run_bulk_assign_async` helper function following the `_run_assign_async` pattern
- Implemented `bulk_assign` command with all CLI options (csv_file, --dry-run, --continue-on-error, --default-location, --output, --yes)
- Added CSV parsing via `parse_assign_csv()` with error handling
- Implemented async validation via `BulkOperations.validate_assign_csv()`
- Added validation summary display (human-readable and JSON modes)
- Implemented dry-run exit with appropriate exit codes (0 for success, 1 for errors)
- Added validation error handling without --continue-on-error (raises ValidationError exit code 5)
- Added confirmation prompt with --yes bypass option
- Added placeholder for execution phase (subtask 27.2)

#### Files Modified
- `src/teams_phone/cli/numbers.py` - Added `bulk_assign` command (lines 930-1115) and `_run_bulk_assign_async` helper (lines 131-164)
- `tests/unit/test_cli_numbers.py` - Added `TestNumbersBulkAssignCommand` class with 12 unit tests

#### Key Decisions Made
- **Exit codes**: dry-run exits with 0 (no errors) or 1 (errors present); validation failure without --continue-on-error raises ValidationError (exit code 5)
- **Default location application**: Applied to rows before validation, not during - allows validation to run on final state
- **JSON output path**: When --dry-run and --json, exit immediately after outputting JSON validation results

#### Patterns Discovered
- Multi-service async helper pattern: `_run_bulk_assign_async` takes `(BulkOperations, ConfigManager)` - see `numbers.py:131-164`
- Confirmation prompt pattern: Check `not yes and not cli_ctx.json_output` before prompting - see `numbers.py:1097-1106`

#### Open Items for Next Agent
- [ ] Subtask 27.2: Implement execution phase with progress bar and actual number assignments
- [ ] Subtask 27.3: Implement error handling and results file output
- [ ] The `--output` option is accepted but not yet used (execution phase needed)

#### Gotchas and Warnings
- ANSI codes may be present in CLI output even with `--no-color` when testing - use substring assertions that don't depend on exact formatting
- The `--yes` flag skips the confirmation prompt which shows "skipped rows" info; if testing for that message, don't use `--yes`

---

### Session: 2026-01-29 - Agent: claude-sonnet-4-5-20250929

**Status:** Subtask 27.2 Implemented

#### What Was Done (Subtask 27.2: Progress Bar with Real-Time Success/Fail Counts)
- Replaced placeholder at lines 1107-1112 with full execution phase implementation
- Added `BulkAssignResult` import to bulk_operations imports
- Implemented progress bar using `formatter.progress()` context manager
- Progress description updates in real-time: `Processing [{current}/{total}] OK:{success} FAIL:{fail}`
- For each validated row:
  - Re-resolves user to get user_id via `bulk_ops.user_service.resolve_user()`
  - Re-fetches number to get number_type via `bulk_ops.number_service.get_number()`
  - Resolves location if provided via `bulk_ops.location_service.validate_location()`
  - Calls `number_service.assign_number()` with `wait=True`
- Handles operation success (status == SUCCEEDED) and failure
- Catches `TeamsPhoneError` exceptions per-row
- Creates `BulkAssignResult` for each processed row
- Pre-populates skipped results for rows with validation errors
- Handles KeyboardInterrupt gracefully, marking remaining rows as "Cancelled by user"
- Displays execution summary: Successful, Failed, Skipped counts
- Exit code 1 if any failures, 0 otherwise

#### Files Modified
- `src/teams_phone/cli/numbers.py:1107-1255` - Replaced placeholder with execution phase implementation
- `tests/unit/test_cli_numbers.py` - Updated 2 existing tests, added 5 new tests for execution phase

#### Key Decisions Made
- **Re-resolution during execution**: User and number are re-resolved during execution phase (not cached from validation) since validation doesn't store resolved IDs
- **Progress bar task_id access**: Use `list(progress.task_ids)[0]` to get the task_id added by `formatter.progress()` context manager
- **ASCII progress format**: Used `OK:` and `FAIL:` labels instead of Unicode checkmarks for Windows compatibility
- **Exit code behavior**: Exit 1 if fail_count > 0, exit 0 otherwise (skipped rows don't count as failures)

#### Patterns Discovered
- Progress context manager access: `formatter.progress(total, description)` yields Progress; task_id via `list(progress.task_ids)[0]`
- Async execution inside progress: Define async function inside `with progress:` block, call via `_run_bulk_assign_async()`
- BulkAssignResult status values: 'success', 'failed', 'skipped' (string literals, not enum)

#### Open Items for Next Agent
- [ ] Subtask 27.3: Implement results file output using `write_results_csv()` from bulk_operations.py
- [ ] The `--output` option is accepted but results file is not yet written

#### Gotchas and Warnings
- **ANSI codes in test assertions**: Even with `--no-color`, Rich adds ANSI codes around numbers. Use substring assertions like `"Successful:" in result.stdout` instead of `"Successful: 1"`.
- **Enum comparison**: Use `operation.status == OperationStatus.SUCCEEDED` (not string comparison)
- **CLIContext not dict**: Cannot assign to `ctx.obj["key"]` - CLIContext is not a dict

---

### Session: 2026-01-29 - Agent: claude-sonnet-4-5-20250929

**Status:** Subtask 27.3 Implemented

#### What Was Done (Subtask 27.3: Continue-on-Error Logic, Default Location Fallback, Results File Output)
- Added `datetime` import and `write_results_csv` import from bulk_operations
- Added `ConfigurationError` import for handling file write errors
- Implemented default location fallback from tenant profile:
  - CLI `--default-location` option takes priority
  - Falls back to tenant profile's `default_location` if CLI option not provided
  - Validates resolved default location via `LocationService.validate_location()`
  - Displays info message when using tenant default location (in non-JSON mode)
- Implemented continue-on-error stop logic:
  - When `--continue-on-error` is NOT set, breaks execution loop on first error
  - Marks remaining unprocessed rows as "skipped" with error "Stopped due to previous error"
  - Works for both operation failures and exception-based errors
- Implemented results file output:
  - Uses `--output` path if provided, otherwise generates timestamp-based filename
  - Default filename: `bulk-assign-results-{YYYYMMDD-HHMMSS}.csv`
  - Writes using `write_results_csv()` utility function
  - Handles `ConfigurationError` gracefully (warns but doesn't fail if assignments succeeded)
- Implemented JSON output for execution results:
  - Returns structured object with `status`, `summary`, `results_file`, and `results` array
  - Summary includes: total, successful, failed, skipped counts
  - Results array includes all rows with status and error details

#### Files Modified
- `src/teams_phone/cli/numbers.py:8` - Added `datetime` import
- `src/teams_phone/cli/numbers.py:17` - Added `ConfigurationError` import
- `src/teams_phone/cli/numbers.py:32` - Added `write_results_csv` import
- `src/teams_phone/cli/numbers.py:1010-1031` - Implemented tenant default location fallback
- `src/teams_phone/cli/numbers.py:1228-1235` - Added continue-on-error stop logic (operation failure)
- `src/teams_phone/cli/numbers.py:1244-1248` - Added continue-on-error stop logic (exception)
- `src/teams_phone/cli/numbers.py:1264-1275` - Added skipped rows for early stop
- `src/teams_phone/cli/numbers.py:1288-1336` - Replaced TODO with results file and JSON output
- `tests/unit/test_cli_numbers.py:2649-2880` - Added 5 new tests for subtask 27.3

#### Key Decisions Made
- **Default location priority**: CLI `--default-location` > tenant profile `default_location` > none
- **Default location validation**: Validates the resolved default location before applying to rows (fails fast if invalid)
- **Results file write timing**: Write results after execution completes, even if some rows failed
- **JSON output structure**: Includes full results array (not just summary) for automation use cases
- **Error handling on write failure**: Warn user but don't change exit code if assignments succeeded

#### Patterns Discovered
- ConfigManager access pattern: `cli_ctx.get_config_manager()` → `config_manager.get_default_tenant()` → `config_manager.get_tenant(name)`
- Tenant profile access: `TenantProfile.default_location` is `Optional[str]`
- Timestamp filename format: `datetime.now().strftime("%Y%m%d-%H%M%S")`

#### Tests Added
- `test_bulk_assign_default_location_from_tenant_profile` - Verifies tenant profile fallback
- `test_bulk_assign_stops_on_first_error_without_continue` - Verifies stop behavior
- `test_bulk_assign_writes_results_file` - Verifies results file output
- `test_bulk_assign_json_output_includes_results` - Verifies JSON structure
- `test_bulk_assign_generates_timestamp_filename` - Verifies default filename format

#### Gotchas and Warnings
- **Mock ConfigManager location**: Patch at `teams_phone.infrastructure.ConfigManager`, not `teams_phone.cli.context.ConfigManager`
- **datetime mock**: Must mock `teams_phone.cli.numbers.datetime` (not global datetime) to control timestamp in tests
- **Results written flag**: Track whether results file was written to show path in output conditionally

---
