# Task Recon: Implement CSV parsing and validation for policy bulk-assign

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

---

## Task Summary

Create CSV parsing logic for policy bulk-assign with `user`, `policy_type`, `policy_name` columns and validate each row against `PolicyService.validate_policy` before execution. This subtask focuses on:

1. Parsing CSV files with required columns
2. Resolving policy types from CLI aliases to API names
3. Validating policies exist in the cache
4. Implementing dry-run preview and confirmation prompts

### Parent Task Context

**Parent Task:** Implement Policies Bulk-Assign Command (ID: 31)
**Parent Goal:** Add bulk-assign command to policies CLI for assigning policies to multiple users from CSV.
**This Subtask's Role:** This subtask handles the input/validation phase - parsing CSV input, validating policy existence, and providing dry-run/confirmation functionality. Subtask 31.2 will handle the actual execution with progress display and results output.

### Dependencies
- Depends on: 24 (PolicyService - DONE), 25 (Policies CLI Commands - DONE), 26 (Bulk CSV Parsing and Validation - DONE), 41 (Validation Checkpoint 3 - DONE)

### Acceptance Criteria
1. CSV parsing for `user`, `policy_type`, `policy_name` columns
2. Policy type resolution using `_resolve_policy_type()` (CLI aliases to API names)
3. Policy validation using `PolicyService.validate_policy()`
4. Pre-validation phase that validates ALL rows and shows validation summary
5. Dry-run mode shows table preview without API calls
6. Confirmation prompt (unless `--yes`) before proceeding

---

## Insights from Previous Tasks

### Dependency Task Notes
| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 24 | PolicyService | `validate_policy(policy_type, policy_name)` requires BOTH type AND name (case-insensitive). Use `TeamsVoicemailPolicy` check first - it's NOT supported for batch assignment. |
| 25 | Policies CLI Commands | Policy type mapping exists at `policies.py:33-41` - `POLICY_TYPE_MAP` dict. `_resolve_policy_type()` at line 47-67 handles CLI alias to API name conversion. |
| 26 | Bulk CSV Parsing | `parse_assign_csv()` is a standalone function, not a class method. Use `utf-8-sig` encoding for BOM handling. Row numbers are 1-indexed starting at 2 (after header). |
| 41 | Validation Checkpoint 3 | TeamsVoicemailPolicy is NOT supported - must use PowerShell. Policy policyId format issue was fixed with runtime resolution in `PolicyService.resolve_policy_id()`. |

### Reusable Patterns from Dependencies
- **CSV parsing pattern**: `parse_assign_csv()` in `bulk_operations.py:80-176` - use this as template for policy CSV parsing
- **Policy type resolution**: `_resolve_policy_type()` in `policies.py:47-67` - already implemented, reuse directly
- **Validation pattern**: `BulkOperations.validate_assign_csv()` in `bulk_operations.py:268-347` - similar async validation approach
- **Sync service pattern**: `_get_sync_service()` in `policies.py:133-143` - for cache-only validation operations

### Warnings and Gotchas
- **TeamsVoicemailPolicy NOT supported**: Must check and reject this policy type early with a clear error message pointing to PowerShell cmdlet
- **Policy.description can be empty string**: Not None - handle accordingly in displays
- **CacheManager.read_policies_csv() returns dict[str, str]**: All values are strings, boolean conversion needed
- **LocationService is sync, others async**: `validate_policy()` is synchronous since it only reads from cache

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

---

## Architecture Context

### Relevant Design Decisions
- **Layered Architecture**: CLI commands -> Services -> Infrastructure (GraphClient, CacheManager)
- **CSV cache for policies**: Graph API has no endpoint for listing policy definitions, so we use PowerShell-exported CSV
- **Sequential bulk operations**: For better progress UX and error handling (not batch API)

### Module Structure
This implementation extends `src/teams_phone/cli/policies.py` which already contains:
- `policies_app` - Typer command group
- `list`, `show`, `assign` commands
- Helper functions: `_resolve_policy_type()`, `_get_sync_service()`, `_run_async()`

### Conventions to Follow
- Use `@policies_app.command("bulk-assign")` decorator
- Follow same parameter patterns as `numbers bulk-assign` command
- Use `typer.Argument` for required positional args, `typer.Option` for flags
- Error handling via `TeamsPhoneError` exceptions with exit codes
- JSON output mode via `cli_ctx.json_output`

---

## Related Code References

### Primary References (Must Read)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/cli/policies.py` | 313-446 | Existing `assign` command - shows user resolution, policy validation, dry-run pattern |
| `src/teams_phone/cli/numbers.py` | 1137-1552 | `bulk-assign` command - comprehensive example of CSV parsing, validation, progress, results |
| `src/teams_phone/services/bulk_operations.py` | 23-78 | `BulkAssignRow`, `BulkValidationResult`, `BulkAssignResult` dataclasses |
| `src/teams_phone/services/bulk_operations.py` | 80-176 | `parse_assign_csv()` function - CSV parsing pattern to adapt |
| `src/teams_phone/services/policy_service.py` | 105-137 | `validate_policy()` method for policy existence check |

### Secondary References (Helpful Context)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/cli/policies.py` | 33-67 | `POLICY_TYPE_MAP` and `_resolve_policy_type()` - reuse for policy type aliases |
| `src/teams_phone/cli/policies.py` | 133-150 | `_get_sync_service()` and `_check_staleness()` - cache-only service pattern |
| `src/teams_phone/infrastructure/output_formatter.py` | 191-227 | `progress()` context manager for progress bars |
| `tests/unit/test_cli_policies.py` | all | Existing test patterns for policies CLI |
| `tests/unit/test_bulk_operations.py` | all | Test patterns for CSV parsing and validation |

### Code Patterns to Follow

```python
# CSV parsing pattern from bulk_operations.py:80-176
# Adapt for policy CSV columns: user, policy_type, policy_name
def parse_policy_csv(file_path: Path) -> list[BulkPolicyRow]:
    required_columns = {"user", "policy_type", "policy_name"}

    with file_path.open(encoding="utf-8-sig") as f:
        reader = csv.DictReader(f)
        # ... validation and row parsing
```

```python
# Dry-run pattern from policies.py:387-406
if dry_run:
    if cli_ctx.json_output:
        formatter.json({
            "dry_run": True,
            # ... preview data
        })
    else:
        formatter.info("Dry Run Preview:")
        # ... table display
```

```python
# Confirmation pattern from policies.py:409-418
if not yes and not cli_ctx.json_output:
    formatter.info(f"Ready to assign {valid_count} policy/policies")
    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()` for CLIContext
- `teams_phone.cli.policies` - `POLICY_TYPE_MAP`, `_resolve_policy_type()`, `_get_sync_service()`
- `teams_phone.services.policy_service` - `PolicyService.validate_policy()`
- `teams_phone.exceptions` - `TeamsPhoneError`, `ValidationError`, `NotFoundError`

### External Packages
- `typer` - CLI framework (already in use)
- `csv` - Standard library CSV parsing

### New Dependencies Needed
None required - all needed packages already in use

---

## Testing Guidance

### Test File Location
`tests/unit/test_cli_policies.py` - extend existing file with new `TestPoliciesBulkAssignCommand` class

### Test Patterns in Use
```python
# Pattern from test_cli_policies.py:386-418
@pytest.mark.unit
def test_assign_dry_run(self) -> None:
    """assign with --dry-run shows preview without making changes."""
    user = _make_user()
    policy = _make_policy()

    with (
        patch("teams_phone.cli.policies._run_async") as mock_run_async,
        patch("teams_phone.cli.policies._get_sync_service") as mock_get_service,
    ):
        # ... setup mocks
        result = runner.invoke(
            app,
            ["--no-color", "policies", "bulk-assign", "test.csv", "--dry-run"],
        )
        assert result.exit_code == 0
        assert "Dry Run" in result.stdout
```

### Fixtures Available
- `_make_policy()` - Create Policy test object
- `_make_user()` - Create UserConfiguration test object
- `_make_assignment()` - Create EffectivePolicyAssignment test object
- `tmp_path` - pytest fixture for temporary CSV files

### Test Utilities
- `tests/conftest.py` - Custom markers: `@pytest.mark.unit`, `@pytest.mark.integration`
- `typer.testing.CliRunner` - CLI invocation testing

---

## Risks and Blockers

### Technical Risks
- [x] **TeamsVoicemailPolicy rejection**: Must validate policy type early and reject voicemail policies with clear error message
- [x] **Policy type alias resolution**: Must handle both CLI aliases (e.g., "calling") and API names (e.g., "TeamsCallingPolicy")

### Dependency Risks
- [x] All dependency tasks are complete (24, 25, 26, 41)

### Open Questions
- None identified - task requirements are clear from CLI specification

---

## Implementation Recommendations

### Suggested Approach
1. **Create dataclasses for policy bulk operations** (similar to `BulkAssignRow` but for policies):
   - `BulkPolicyRow`: `row_number`, `user`, `policy_type`, `policy_name`, `errors`, `warnings`
   - `BulkPolicyValidationResult`: `rows`, `valid_count`, `error_count`, `warning_count`

2. **Create `parse_policy_csv()` function**:
   - Validate required columns: `user`, `policy_type`, `policy_name`
   - Parse rows with error collection per row
   - Return list of `BulkPolicyRow`

3. **Create validation logic**:
   - Resolve policy type using `_resolve_policy_type()`
   - Check for TeamsVoicemailPolicy and add error
   - Call `PolicyService.validate_policy()` for each row
   - Collect all errors before stopping

4. **Add `bulk-assign` command to `policies_app`**:
   - Parse CSV file
   - Validate all rows (pre-validation phase)
   - Display validation summary
   - Handle dry-run output (table preview)
   - Confirmation prompt (unless --yes)
   - Return validated rows for subtask 31.2 to execute

### Files to Create/Modify
| Action | File | Reason |
|--------|------|--------|
| Modify | `src/teams_phone/cli/policies.py` | Add `bulk-assign` command and helper functions |
| Modify | `src/teams_phone/services/bulk_operations.py` | Add `BulkPolicyRow`, `BulkPolicyValidationResult` dataclasses and `parse_policy_csv()` function |
| Modify | `tests/unit/test_cli_policies.py` | Add tests for bulk-assign command |
| Modify | `tests/unit/test_bulk_operations.py` | Add tests for policy CSV parsing |

### Quick Wins
- Reuse `_resolve_policy_type()` from existing code - already handles CLI alias to API name mapping
- Reuse `_get_sync_service()` for cache-only validation - no need for full async context
- Pattern the command signature after `numbers bulk-assign` for consistency

---

## Cheat Sheet

### Key Imports
```python
# In policies.py - already present
from teams_phone.cli.context import get_context
from teams_phone.exceptions import TeamsPhoneError, ValidationError, NotFoundError
from teams_phone.services.policy_service import PolicyService

# Need to add
import csv
from pathlib import Path
from dataclasses import dataclass, field
```

### Key Functions to Use
- `_resolve_policy_type(policy_type: str | None) -> str | None` - Convert CLI alias to API name
- `_get_sync_service() -> PolicyService` - Create PolicyService for cache-only operations
- `PolicyService.validate_policy(policy_type, policy_name) -> Policy` - Validate policy exists
- `formatter.table(data, columns, title)` - Display table output
- `typer.confirm(message) -> bool` - Confirmation prompt

### Common Patterns
- **CSV parsing**: See `bulk_operations.py:80-176`
- **Validation summary**: See `numbers.py:1279-1308`
- **Dry-run output**: See `policies.py:387-406`
- **Confirmation prompt**: See `policies.py:409-418`
- **Error handling**: Catch `TeamsPhoneError`, call `formatter.error()`, raise `typer.Exit(e.exit_code)`

### CLI Command Signature (from spec)
```python
@policies_app.command("bulk-assign")
def bulk_assign(
    ctx: typer.Context,
    csv_file: Path = typer.Argument(..., help="CSV with user, policy_type, policy_name columns"),
    dry_run: bool = typer.Option(False, "--dry-run"),
    continue_on_error: bool = typer.Option(False, "--continue-on-error"),
    output: Optional[Path] = typer.Option(None, "--output", "-o"),
    yes: bool = typer.Option(False, "--yes", "-y"),
):
    """Bulk assign policies from CSV file."""
```

### CSV Format Expected
```csv
user,policy_type,policy_name
john.doe@contoso.com,calling,AllowCalling
jane.smith@contoso.com,voicemail,TranscriptionEnabled
```

---

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