# Task ID: 41

**Title:** Validation Checkpoint 3: Write Operations

**Status:** done

**Dependencies:** 19, 20, 21, 22, 23, 24, 25

**Priority:** high

**Description:** Validate Phase 3 completion by testing all modification operations including number assignment, unassignment, and policy assignment. This checkpoint gates entry into Phase 4 (Bulk Operations).

**Details:**

**Validation Criteria:**
- [x] `numbers assign` completes async operation successfully
- [x] E911 enforcement blocks Calling Plan without location
- [x] `numbers unassign` completes successfully
- [x] `policies assign` applies policy correctly (with caveats - see bugs)
- [x] --dry-run shows preview without changes
- [x] --force skips confirmation prompts

**Test Execution:**
1. Assign Direct Routing number without location (should succeed)
2. Attempt to assign Calling Plan number without location (should fail with E911 error)
3. Assign Calling Plan number WITH location, verify async operation completes
4. Run `users show` to verify number appears in user config
5. Unassign number with confirmation prompt, test --force flag
6. Assign policy to user, verify in `users show` output
7. Test --dry-run on assign/unassign operations, verify no changes made

**Success Criteria:** All write operations complete successfully, E911 enforcement works, async polling completes, dry-run mode previews without executing, confirmations work.

**Test Strategy:**

Integration tests against test tenant with test users and available numbers. Verify async operation polling, E911 validation, and policy assignment.

---

## Session Notes (2026-01-29)

### Test Results

| Test | Status | Notes |
|------|--------|-------|
| DR number assign without location | PASSED | Works correctly with `--no-wait` |
| `--dry-run` on assign | PASSED | Shows preview, no changes made |
| `numbers unassign` with `--force` | PASSED | After fixing Location header bug |
| `--no-wait` for async operations | PASSED | Returns immediately with operation ID |
| E911 enforcement (Calling Plan) | PASSED | Correctly rejects without location |
| Calling Plan with location | PASSED | Assignment completes |
| Policy assignment | PASSED | Runtime policyId resolution implemented |

### Bugs Found and Fixed

#### 1. Location Header Parsing (FIXED)

**File:** `src/teams_phone/services/number_service.py:268`

**Issue:** Graph API returns Location header in REST path format (`/operations/{id}`) but code expected OData function format (`/operations('{id}')`).

**Fix:** Updated regex to support both formats:
```python
# Try OData function style first: operations('{id}')
pattern = r"operations\('([^']+)'\)"
match = re.search(pattern, location_header)

# Fall back to REST path style: operations/{id}
if not match:
    pattern = r"operations/([^/\s]+)$"
    match = re.search(pattern, location_header)
```

#### 2. Improved Error Messages for 500 Errors (FIXED)

**File:** `src/teams_phone/infrastructure/graph_client.py:141-159`

**Issue:** When Graph API returned 500 errors after retry exhaustion, the error message didn't include the actual API error details.

**Fix:** Extract and include error details from response body in the exception message.

### Bugs Found (Not Yet Fixed)

#### 3. Windows Unicode Encoding (FIXED)

**Impact:** High - CLI crashes on Windows

**Issue:** Rich library spinners and checkmarks use Unicode characters (Braille dots like `⠙`, checkmark `✓`) that cannot be encoded by Windows console's cp1252 encoding.

**Symptoms:**
- Crashes during async operation polling (spinner)
- Crashes when displaying success messages (checkmark)
- Error: `UnicodeEncodeError: 'charmap' codec can't encode character '\u2713'`

**Fix:** Auto-detect Unicode support and fall back to ASCII-safe alternatives.

**Files Changed:**
- `src/teams_phone/infrastructure/output_formatter.py`
  - Added `_can_encode_unicode()` helper to detect console encoding
  - Added `_use_ascii` flag to OutputFormatter
  - `success()` uses `OK:` instead of `✓` on Windows legacy console
  - `get_spinner_name()` returns `line` (ASCII) instead of `dots` (Unicode Braille)
  - Added `placeholder` property returning `-` instead of `—` for ASCII mode
- `src/teams_phone/cli/numbers.py`
  - Updated `_run_with_progress()` to use `formatter.get_spinner_name()`
- `tests/unit/test_output_formatter.py`
  - Added `TestAsciiFallback` class with 12 new tests

#### 4. Policy policyId Format (FIXED)

**Impact:** Critical - Policy assignment only works with hardcoded IDs

**Issue:** PowerShell CSV export stores policy *names* (e.g., "AllowCalling") as `policyId`, but Graph API `/admin/teams/policy/userAssignments/assign` endpoint requires actual base64-encoded policyIds (e.g., `c4K97LmKm0n2t-5_plHB8sg7iRK6dM5NwTfbtZWKlpM`).

**Fix:** Implemented runtime policyId resolution instead of modifying PowerShell script.

**Files Changed:**
- `src/teams_phone/services/policy_service.py`
  - Added `resolve_policy_id()` method that queries user configurations to find the real policyId
  - Modified `assign_policy()` to use resolved policyId instead of CSV value
  - Added `_strip_odata_metadata()` helper to handle @odata.context in API responses
- `src/teams_phone/services/user_service.py`
  - Added same `_strip_odata_metadata()` helper for consistency
- `tests/unit/test_policy_service.py`
  - Updated tests to mock policyId resolution flow
  - Added test for "policy not assigned to any user" error case

**Behavior:**
- CSV still used for validation and display (policy exists check)
- Real policyId resolved at runtime from tenant user assignments
- Error if no user in tenant has the policy assigned (provides remediation guidance)

#### 5. OData Metadata in Single-Item Responses (FIXED)

**Issue:** Graph API single-item responses include `@odata.context` field which Pydantic rejects with `extra="forbid"`.

**Fix:** Added `_strip_odata_metadata()` helper in both services to strip `@`-prefixed fields before validation.

### Tenant State After Testing

- `scoobie@hvk-consulting.com` - No number, AllowCallingPreventForwardingtoPhone policy assigned
- `frank@hvk-consulting.com` - +13318673766 (Calling Plan), AllowCallingPreventForwardingtoPhone policy assigned
- `jami@hvk-consulting.com` - +12025551212 (DR), AllowCallingPreventForwardingtoPhone policy assigned

### Next Steps

1. ~~Create task for Windows Unicode encoding fix~~ DONE
2. Validation checkpoint ready for completion - all bugs fixed
