# Task ID: 26

**Title:** Implement Bulk CSV Parsing and Validation

**Status:** pending

**Dependencies:** 10, 15, 17

**Priority:** medium

**Description:** Create utilities for parsing and validating bulk operation CSV files with dry-run support.

**Details:**

Create `src/teams_phone/services/bulk_operations.py`:
```python
from dataclasses import dataclass

@dataclass
class BulkAssignRow:
    row_number: int
    user: str
    phone_number: str
    location: str | None
    errors: list[str]
    warnings: list[str]

@dataclass
class BulkValidationResult:
    rows: list[BulkAssignRow]
    valid_count: int
    error_count: int
    warning_count: int

class BulkOperations:
    def __init__(self, user_service: UserService, number_service: NumberService, location_service: LocationService): ...
    
    def parse_assign_csv(self, file_path: Path) -> list[BulkAssignRow]:
        """Parse CSV with columns: user, phone_number, location (optional)."""
        ...
    
    async def validate_assign_csv(self, rows: list[BulkAssignRow]) -> BulkValidationResult:
        """Validate all rows: user exists, number available, location valid."""
        ...
    
    def write_results_csv(self, results: list[BulkAssignResult], output_path: Path) -> None:
        """Write results with: row, user, phone_number, status, error."""
        ...
```

**Test Strategy:**

Unit tests: CSV parsing with various formats, validation logic, results file generation, handling of missing columns, encoding issues.

---

## Subtask 26.1: Implement CSV Parsing with BulkAssignRow Dataclass

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

**Status:** Implemented

#### What Was Done
- Created `src/teams_phone/services/bulk_operations.py` with:
  - `BulkAssignRow` dataclass with proper `field(default_factory=list)` for mutable defaults
  - `BulkValidationResult` dataclass for validation results
  - `parse_assign_csv()` function for parsing bulk assignment CSV files
- Created `tests/unit/test_bulk_operations.py` with 17 comprehensive unit tests covering:
  - Dataclass field storage and default initialization
  - Mutable default independence (no shared state)
  - Valid CSV parsing with all columns
  - CSV without optional location column
  - UTF-8 BOM handling (PowerShell exports)
  - Empty required field error collection
  - Empty optional field warning generation
  - Missing column validation (raises ValidationError)
  - Header-only and empty file handling
  - Missing file error handling
  - 1-indexed row numbers
  - Whitespace stripping

#### Key Decisions Made
- **Dataclass vs Pydantic**: Used dataclass as specified in task requirements (these are internal data structures, not API contracts)
- **Standalone function**: Implemented `parse_assign_csv` as a standalone function rather than a class method, to be integrated into `BulkOperations` class in subtask 26.2
- **Row numbering**: Used 1-indexed row numbers starting at 2 (after header) for user-facing display
- **Empty string handling**: Treating empty strings in required fields as errors (added to row.errors list)
- **Location warnings**: Only generating warnings for empty location when the column exists in the CSV

#### Patterns Discovered
- CSV reading pattern from `cache_manager.py:150-174` - used `utf-8-sig` encoding for BOM handling
- ValidationError with remediation from `exceptions.py:85-95` - included user guidance in all errors
- Test CSV data pattern from `test_location_service.py:14-16` - multiline strings for test data

#### Open Items for Next Agent
- [ ] Subtask 26.2: Create `BulkOperations` class and implement `validate_assign_csv()` async method
- [ ] Subtask 26.3: Implement `write_results_csv()` method

#### Gotchas and Warnings
- **Circular import in isolation**: When running `tests/unit/test_bulk_operations.py` alone (without other test files), a circular import error occurs due to the existing `services/__init__.py` initialization order. This is a pre-existing codebase issue - tests pass when run via `pytest tests/unit/` (alphabetical loading resolves the cycle).
- **Row numbers start at 2**: The first data row is row 2 (row 1 is the header). This matches user expectations when viewing the CSV in a spreadsheet.
- **CSV DictReader returns strings**: All values from csv.DictReader are strings - explicit conversion needed for any non-string types.

---

## Subtask 26.2: Implement Async Validation Logic

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

**Status:** Implemented

#### What Was Done
- Added `BulkOperations` class to `src/teams_phone/services/bulk_operations.py` with:
  - Constructor with dependency injection for `UserService`, `NumberService`, `LocationService`
  - `validate_assign_csv()` async method implementing full validation logic
- Added 14 new unit tests to `tests/unit/test_bulk_operations.py` covering:
  - Valid row passes validation
  - User not found adds error
  - Ambiguous user match adds error
  - Number already assigned adds error
  - Number not in inventory adds error
  - Invalid location adds error
  - Stale cache adds warning
  - Calling Plan without location adds warning
  - Skips validation for rows with parsing errors
  - Counts computed correctly (valid, error, warning)
  - Multiple errors per row
  - Empty rows returns zero counts
  - Location not validated when None

#### Key Decisions Made
- **Sequential processing**: Chose sequential processing over `asyncio.gather` for rate limit safety (can optimize later if needed)
- **Skip rows with parsing errors**: Rows that already have errors from parsing (missing user/phone_number) are skipped in validation
- **Collect all errors**: Each row can accumulate multiple errors (user, number, location) rather than stopping at first
- **Stale cache warning per row**: Added stale cache warning to each row rather than just once, for visibility
- **TYPE_CHECKING imports**: Used `TYPE_CHECKING` guard to avoid circular imports for service types

#### Patterns Discovered
- Service method patterns from `user_service.py:191-265` - `resolve_user()` raises `NotFoundError` or `ValidationError`
- Service method patterns from `number_service.py:116-150` - `get_number()` raises `NotFoundError`
- Location validation from `location_service.py:101-127` - `validate_location()` raises `NotFoundError` (sync)
- Cache staleness check from `location_service.py:166-172` - `is_cache_stale()` returns bool (sync)
- Mock async service pattern using `AsyncMock` for async methods, `MagicMock` for sync methods

#### Open Items for Next Agent
- [ ] Subtask 26.3: Implement `write_results_csv()` method for output file generation

#### Gotchas and Warnings
- **LocationService is sync**: `validate_location()` and `is_cache_stale()` are synchronous methods while `resolve_user()` and `get_number()` are async. Handle appropriately in async context.
- **Circular import still exists**: Run tests via `pytest tests/unit/` not `pytest tests/unit/test_bulk_operations.py` alone.
- **NumberType.CALLING_PLAN**: Use `NumberType.CALLING_PLAN` enum value, not string `"callingPlan"` for comparison.
- **AssignmentStatus.UNASSIGNED**: Compare against enum, not string. Number is available only when status is `UNASSIGNED`.

---

## Subtask 26.3: Implement Results File Generation

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

**Status:** Implemented

#### What Was Done
- Added `BulkAssignResult` dataclass to `src/teams_phone/services/bulk_operations.py`:
  - Fields: `row_number`, `user`, `phone_number`, `status`, `error` (optional)
  - Status values: 'success', 'failed', 'skipped'
  - Default error to None
- Implemented `write_results_csv()` standalone function:
  - Creates parent directories if they don't exist
  - Writes UTF-8 without BOM
  - Uses `csv.DictWriter` with explicit fieldnames for consistent column ordering
  - Handles PermissionError for both directory creation and file writing
  - Raises `ConfigurationError` with remediation guidance on permission failures
- Added `ConfigurationError` import to module
- Added 11 new unit tests to `tests/unit/test_bulk_operations.py`:
  - `TestBulkAssignResult` (3 tests): field storage, default error, status values
  - `TestWriteResultsCsv` (8 tests): valid output, UTF-8 without BOM, directory creation, permission errors (mkdir and write), empty results, multiple errors formatting, field ordering

#### Key Decisions Made
- **Standalone function**: Implemented as standalone function (like `parse_assign_csv`) rather than class method for consistency
- **Output column name 'row'**: Task spec required 'row' not 'row_number' in output CSV header
- **UTF-8 without BOM**: Output uses plain UTF-8 (no BOM) since BOM is only needed for PowerShell compatibility on input
- **Empty error as empty string**: When error is None, output empty string in CSV rather than 'None'
- **newline=""**: Used `newline=""` in `open()` to ensure consistent line endings per csv module docs

#### Patterns Discovered
- PermissionError handling pattern from `cache_manager.py:54-71` - `ConfigurationError` with remediation
- CSV writing pattern using `DictWriter` for ordered field output
- Test pattern for PermissionError from `test_cache_manager.py:73-84` - `patch.object(Path, ...)`

#### Open Items for Next Agent
- Task 26 is complete - all 3 subtasks implemented
- Future work: CLI integration in Task 28 will use these utilities for `numbers assign --csv` command

#### Gotchas and Warnings
- **Multiple errors format**: Caller is responsible for joining multiple errors with `"; "` delimiter before passing to `BulkAssignResult`. The `write_results_csv` function preserves whatever error string is provided.
- **Circular import persists**: Continue running tests via `pytest tests/unit/` not individual test files.
- **Column order**: Output columns are `row,user,phone_number,status,error` - order is enforced by `fieldnames` list.

---
