# Task Recon: Implement list/get/add/remove tenant operations with ConfigManager

**Task ID:** 13.1
**Status:** pending
**Generated:** 2026-01-28T12:00:00Z
**Agent:** claude-opus-4-5-20251101

---

## Task Summary

Create the TenantService class with CRUD operations that delegate to ConfigManager for persistence of TenantProfile objects. This includes implementing `list_tenants()`, `get_current_tenant()`, `add_tenant()`, and `remove_tenant()` methods.

### Parent Task Context

**Parent Task:** Implement TenantService (ID: 13)
**Parent Goal:** Create the TenantService for managing tenant profiles including list, add, remove, switch, and connection testing. The complete TenantService will coordinate with both ConfigManager (for profile persistence) and AuthService (for authentication).
**This Subtask's Role:** This subtask creates the foundational TenantService class with basic CRUD operations, establishing the service pattern and ConfigManager integration. Subsequent subtasks (13.2, 13.3) will add switch_tenant with auto-login and test_connection functionality.

### Dependencies
- Depends on: Task 4 (ConfigManager) - done, Task 6 (AuthService) - done

### Acceptance Criteria
From task details:
1. Create `src/teams_phone/services/tenant_service.py` with TenantService class
2. `__init__(self, config_manager: ConfigManager, auth_service: AuthService)` - store references
3. `list_tenants() -> list[TenantProfile]` - return sorted list of all tenants
4. `get_current_tenant() -> TenantProfile` - return default tenant, raise ConfigurationError if none
5. `add_tenant(profile: TenantProfile) -> TenantProfile` - validate and save, auto-set first as default
6. `remove_tenant(name: str) -> None` - check auth status, delegate to ConfigManager
7. Export TenantService from `services/__init__.py`

---

## Insights from Previous Tasks

### Dependency Task Notes
| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 4 | ConfigManager | All tenant CRUD delegates to `config_manager.get_tenant()`, `set_tenant()`, `remove_tenant()`, `get_default_tenant()`, `set_default_tenant()` |
| 4.2 | Tenant CRUD | ValidationError is raised on name mismatch in set_tenant; NotFoundError for missing tenant |
| 4.3 | Default Tenant | `set_default_tenant` validates tenant exists before setting; `remove_tenant` clears default if it matches |
| 6.4 | Auth Methods | AuthService has `is_authenticated(tenant_name)` to check if token exists and is valid (never throws) |

### Reusable Patterns from Dependencies
- **Load-modify-save pattern**: See `config_manager.py:140-157` - `set_tenant()` and `remove_tenant()` follow this pattern
- **Tenant resolution pattern**: See `auth_service.py:229-256` - `_resolve_tenant()` handles None with default tenant fallback
- **Service class structure**: See `auth_service.py:42-63` - `__init__` stores dependencies, has comprehensive docstrings

### Warnings and Gotchas
- From Task 4: `remove_tenant` must check and clear `default_tenant` before saving - ConfigManager already handles this
- From Task 4: ConfigManager already raises `NotFoundError` for missing tenants - TenantService just delegates
- From Task 6.4: `is_authenticated(None)` returns False if no default tenant - safe to call before remove

### Open Items Inherited
- None directly inherited - Tasks 4 and 6 are complete

---

## Architecture Context

### Relevant Design Decisions
- **Layered Architecture**: TenantService is a Service layer component, coordinating between CLI and Infrastructure (ConfigManager)
- **Repository Pattern**: ConfigManager acts as the repository; TenantService adds business logic on top
- **Dependency Injection**: Services receive their dependencies in `__init__` for testability

### Module Structure
```
src/teams_phone/
├── services/
│   ├── __init__.py          # Exports TenantService
│   ├── auth_service.py      # Existing AuthService pattern
│   └── tenant_service.py    # NEW: TenantService implementation
├── infrastructure/
│   ├── config_manager.py    # ConfigManager (dependency)
│   └── cache_manager.py     # CacheManager (used by AuthService)
├── models/
│   ├── tenant.py            # TenantProfile model
│   └── config.py            # Config model with tenants dict
└── exceptions.py            # ConfigurationError, NotFoundError
```

### Conventions to Follow
- Use `ConfigDict(extra="forbid")` for strict Pydantic models
- Provide `remediation` kwarg in exception constructors
- Include comprehensive docstrings with Args/Returns/Raises
- Use type hints consistently with `str | None` syntax
- Export from `__init__.py` for public API

---

## Related Code References

### Primary References (Must Read)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/services/auth_service.py` | 42-63 | Service class pattern: `__init__` with dependencies, docstrings |
| `src/teams_phone/infrastructure/config_manager.py` | 120-208 | ConfigManager CRUD methods to delegate to |
| `src/teams_phone/models/tenant.py` | 22-46 | TenantProfile model definition |
| `src/teams_phone/models/config.py` | 46-68 | Config model with tenants dict and default_tenant |

### Secondary References (Helpful Context)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/services/__init__.py` | 1-5 | Export pattern for services |
| `src/teams_phone/exceptions.py` | 112-121 | ConfigurationError definition |
| `src/teams_phone/exceptions.py` | 72-82 | NotFoundError definition |
| `tests/unit/test_auth_service.py` | 78-101 | Test class structure for service tests |

### Code Patterns to Follow

```python
# Service class pattern from auth_service.py:42-63
class AuthService:
    """Service for authenticating with Microsoft Entra ID.

    Handles certificate-based and client-secret authentication methods,
    token caching, and authentication status management.

    Attributes:
        config_manager: Manager for tenant configuration.
        cache_manager: Manager for token caching.
    """

    def __init__(
        self, config_manager: ConfigManager, cache_manager: CacheManager
    ) -> None:
        """Initialize the AuthService.

        Args:
            config_manager: ConfigManager instance for tenant configuration.
            cache_manager: CacheManager instance for token storage.
        """
        self.config_manager = config_manager
        self.cache_manager = cache_manager
```

```python
# Exception pattern with remediation from config_manager.py:133-137
raise NotFoundError(
    f"Tenant '{name}' not found",
    remediation="Run 'teams-phone tenants list' to see configured tenants.",
)
```

---

## Dependencies

### Internal Modules
- `teams_phone.infrastructure.ConfigManager` - tenant CRUD operations
- `teams_phone.services.AuthService` - check auth status before removal
- `teams_phone.models.TenantProfile` - tenant data model
- `teams_phone.exceptions.ConfigurationError` - no default tenant error
- `teams_phone.exceptions.NotFoundError` - tenant not found (delegated)

### External Packages
- `pydantic` - TenantProfile validation (already in use)
- No new external packages needed

### New Dependencies Needed
- None required - all needed modules already exist

---

## Testing Guidance

### Test File Location
`tests/unit/test_tenant_service.py` (create new)

### Test Patterns in Use
```python
# Example test structure from test_auth_service.py:78-101
class TestAuthServiceInit:
    """Tests for AuthService initialization."""

    @pytest.mark.unit
    def test_accepts_dependencies(self, tmp_path: Path) -> None:
        """AuthService accepts ConfigManager and CacheManager."""
        config_manager = ConfigManager(config_path=str(tmp_path / "config.toml"))
        cache_manager = CacheManager(token_dir=str(tmp_path / "tokens"))

        service = AuthService(config_manager, cache_manager)

        assert service.config_manager is config_manager
        assert service.cache_manager is cache_manager
```

```python
# Exception testing pattern from test_config_manager.py
class TestGetTenant:
    @pytest.mark.unit
    def test_raises_not_found_for_missing_tenant(self, tmp_path: Path) -> None:
        """get_tenant raises NotFoundError for non-existent tenant."""
        manager = ConfigManager(config_path=str(tmp_path / "config.toml"))

        with pytest.raises(NotFoundError) as exc_info:
            manager.get_tenant("nonexistent")

        assert "nonexistent" in str(exc_info.value)
        assert exc_info.value.remediation is not None
```

### Fixtures Available
- `tmp_path` - pytest built-in for temporary directory
- `monkeypatch` - pytest built-in for mocking environment variables

### Test Utilities
- `tests/conftest.py` - Custom markers: `@pytest.mark.unit`, `@pytest.mark.integration`
- Standard unittest.mock: `MagicMock`, `patch`, `patch.object`

---

## Risks and Blockers

### Technical Risks
- [x] **Low Risk**: Delegation to ConfigManager is straightforward; all CRUD methods already implemented
- [ ] **Medium Risk**: Need to clarify behavior when removing authenticated tenant - should we warn, clear token, or just delete?

### Dependency Risks
- None - Task 4 and Task 6 are complete and tested

### Open Questions
- [ ] Should `remove_tenant()` warn/confirm if the tenant is currently authenticated, or just silently proceed?
- [ ] Should `add_tenant()` validate that required fields (tenant_id, client_id) are present, or rely on Pydantic?
- [ ] Task details mention "check if tenant is currently authenticated and warn/clear token if so" for remove - clarify expected behavior

---

## Implementation Recommendations

### Suggested Approach
1. Create `src/teams_phone/services/tenant_service.py` with TenantService class skeleton
2. Implement `__init__` storing config_manager and auth_service references
3. Implement `list_tenants()` - load config, extract tenants.values(), sort by name
4. Implement `get_current_tenant()` - get default tenant name, raise if None, return profile
5. Implement `add_tenant()` - validate, save via set_tenant, auto-set first as default
6. Implement `remove_tenant()` - check auth status (optional warning), delegate to ConfigManager
7. Update `services/__init__.py` to export TenantService
8. Create `tests/unit/test_tenant_service.py` with comprehensive tests

### Files to Create/Modify
| Action | File | Reason |
|--------|------|--------|
| Create | `src/teams_phone/services/tenant_service.py` | New TenantService class |
| Modify | `src/teams_phone/services/__init__.py` | Export TenantService |
| Create | `tests/unit/test_tenant_service.py` | Unit tests for TenantService |

### Quick Wins
- Start with `list_tenants()` - simplest method, validates ConfigManager integration
- `get_current_tenant()` is nearly identical to AuthService's `_resolve_tenant()` pattern

---

## Cheat Sheet

### Key Imports
```python
from teams_phone.exceptions import ConfigurationError, NotFoundError
from teams_phone.infrastructure import ConfigManager
from teams_phone.models import TenantProfile
from teams_phone.services import AuthService
```

### Key Functions to Use
- `config_manager.load_config() -> Config` - get all configuration
- `config_manager.get_tenant(name) -> TenantProfile` - get single tenant (raises NotFoundError)
- `config_manager.set_tenant(name, profile) -> None` - add/update tenant
- `config_manager.remove_tenant(name) -> None` - delete tenant (clears default if match)
- `config_manager.get_default_tenant() -> str | None` - get default tenant name
- `config_manager.set_default_tenant(name) -> None` - set default (validates exists)
- `auth_service.is_authenticated(name) -> bool` - check if tenant has valid token (never throws)

### Common Patterns
- **Sorted list return**: `sorted(config.tenants.values(), key=lambda t: t.name)`
- **Auto-set first default**: Check `len(config.tenants) == 0` before adding, then set default after
- **ConfigurationError for no default**: Raise with remediation guiding to set default

---

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