# Task Recon: Implement tenant CRUD operations (get/set/remove)

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

---

## Task Summary

Implement `get_tenant()`, `set_tenant()`, and `remove_tenant()` methods in the ConfigManager class for managing TenantProfile entries in the configuration. These methods provide CRUD operations for tenant profiles stored in the TOML config file.

### Parent Task Context

**Parent Task:** Implement ConfigManager (ID: 4)
**Parent Goal:** Create the ConfigManager infrastructure component for reading/writing TOML configuration and managing tenant profiles. The ConfigManager should handle all configuration file operations including tenant management and default tenant settings.
**This Subtask's Role:** Implements the core tenant management operations (get/set/remove) that allow the CLI to add, update, retrieve, and delete tenant profiles. This is the second of three subtasks, building on the TOML loading/saving foundation from subtask 4.1.

### Dependencies
- Depends on: 4.1 (Implement TOML loading/saving with tomllib and tomlkit) - **COMPLETED**

### Acceptance Criteria
From task details:
1. `get_tenant(name: str) -> TenantProfile` - loads config, returns tenant by name, raises `NotFoundError` if not found
2. `set_tenant(name: str, profile: TenantProfile)` - loads config, adds/updates tenant, saves; ensure profile.name matches key
3. `remove_tenant(name: str)` - loads config, removes tenant (raises `NotFoundError` if not present), clears `default_tenant` if it matches removed tenant, saves
4. All methods use `load_config`/`save_config` internally for atomic operations

---

## Insights from Previous Tasks

### Dependency Task Notes

| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 4.1 | Implement TOML loading/saving | `Config.model_dump(mode="json", exclude_none=True)` is the pattern for TOML-safe serialization |
| 4.1 | Implement TOML loading/saving | tomllib.TOMLDecodeError is the exception type for parsing errors |
| 4.1 | Implement TOML loading/saving | Pydantic ValidationError needs wrapping in ConfigurationError with remediation guidance |

### Reusable Patterns from Dependencies
- **TOML serialization pattern**: `config.model_dump(mode="json", exclude_none=True)` - see `config_manager.py:104`
- **Error wrapping pattern**: Wrap PydanticValidationError in ConfigurationError with remediation - see `config_manager.py:76-83`

### Warnings and Gotchas
- **From Task 4.1**: tomlkit throws `ConvertError` on None values - always use `exclude_none=True`
- **From Task 4.1**: mypy python_version is 3.10 but tomllib is 3.11+ stdlib - need ignore comment on fallback import
- **From Task 4.1**: Round-trip test validates that saved config can be loaded back correctly

### Open Items Inherited
- [x] Subtask 4.2: Implement tenant CRUD methods (get_tenant, set_tenant, remove_tenant) - **THIS TASK**
- [ ] Subtask 4.3: Implement default tenant methods (get_default_tenant, set_default_tenant)

---

## Architecture Context

### Relevant Design Decisions
- **Layered Monolith Architecture**: ConfigManager lives in `infrastructure/` layer which handles external concerns (file I/O, config)
- **Error Hierarchy**: Use `NotFoundError` (exit code 4) for missing tenants, `ConfigurationError` (exit code 7) for config issues
- **Atomic Operations**: Each CRUD method should load, modify, save as a single logical operation

### Module Structure
```
src/teams_phone/
├── infrastructure/
│   ├── __init__.py          # Exports ConfigManager
│   └── config_manager.py    # ConfigManager class (modify this)
├── models/
│   ├── config.py            # Config model with tenants dict
│   └── tenant.py            # TenantProfile model
└── exceptions.py            # NotFoundError, ConfigurationError
```

### Conventions to Follow
- Use type hints consistently (`str | None` syntax for Python 3.10+)
- All methods should have docstrings following existing style
- Use `pytest.raises` with `exc_info` for exception tests
- Mark tests with `@pytest.mark.unit`
- Raise exceptions with remediation guidance where helpful

---

## Related Code References

### Primary References (Must Read)

| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/infrastructure/config_manager.py` | 1-107 | Existing ConfigManager with load_config/save_config |
| `src/teams_phone/exceptions.py` | 72-82 | NotFoundError definition to use for missing tenants |
| `src/teams_phone/models/config.py` | 46-68 | Config model showing tenants dict structure |
| `src/teams_phone/models/tenant.py` | 22-47 | TenantProfile model definition |

### Secondary References (Helpful Context)

| File | Lines | Purpose |
|------|-------|---------|
| `tests/unit/test_config_manager.py` | 1-294 | Existing tests to follow as pattern |
| `tests/unit/test_exceptions.py` | 84-88 | NotFoundError test pattern |
| `.taskmaster/docs/architecture/11-error-handling-strategy.md` | 14-17 | NotFoundError variants including TenantNotFoundError |

### Code Patterns to Follow

```python
# Existing load/save pattern from config_manager.py:47-83
def load_config(self) -> Config:
    """Load and validate configuration from the TOML file."""
    if not self.config_path.exists():
        return Config()
    # ... parsing and validation ...
    return Config.model_validate(data)

# Exception pattern from exceptions.py:72-82
class NotFoundError(TeamsPhoneError):
    """Resource not found (exit code 4).

    Raised when a requested resource doesn't exist:
    - User not found in tenant directory
    - Phone number not in inventory
    - Policy not configured
    - Location not defined
    """
    exit_code: int = ExitCode.NOT_FOUND
```

---

## Dependencies

### Internal Modules
- `teams_phone.models.Config` - main config model with `tenants: dict[str, TenantProfile]` and `default_tenant: str | None`
- `teams_phone.models.TenantProfile` - tenant profile model with `name`, `tenant_id`, `client_id`, etc.
- `teams_phone.exceptions.NotFoundError` - exception for missing resources
- `teams_phone.exceptions.ConfigurationError` - exception for config errors (already imported)

### External Packages
- `tomlkit` - already imported for saving (used in save_config)
- `tomllib`/`tomli` - already imported for loading (used in load_config)
- `pydantic` - already imported for model validation

### New Dependencies Needed
None required - all dependencies are already in place from subtask 4.1

---

## Testing Guidance

### Test File Location
`tests/unit/test_config_manager.py` (extend existing file)

### Test Patterns in Use
```python
# Existing test class structure (test_config_manager.py:19-33)
class TestConfigManagerInit:
    """Tests for ConfigManager initialization."""

    @pytest.mark.unit
    def test_default_path(self) -> None:
        """ConfigManager uses default path when not specified."""
        manager = ConfigManager()
        assert manager.config_path == Path.home() / ".teams-phone" / "config.toml"

# Exception test pattern (test_config_manager.py:137-154)
@pytest.mark.unit
def test_invalid_toml_raises_configuration_error(self, tmp_path: Path) -> None:
    """load_config raises ConfigurationError for invalid TOML syntax."""
    # ... setup ...
    with pytest.raises(ConfigurationError) as exc_info:
        manager.load_config()
    assert "Invalid TOML syntax" in str(exc_info.value)
    assert exc_info.value.remediation is not None
```

### Fixtures Available
- `tmp_path` - pytest built-in fixture for temporary directory (used extensively in existing tests)
- `uuid4()` - from uuid module for generating test tenant IDs

### Test Utilities
- `tests/conftest.py` - contains marker registration (unit, integration, contract)
- `from teams_phone.models import AuthMethod, TenantProfile` - for creating test profiles

---

## Risks and Blockers

### Technical Risks
- [ ] **Atomicity concern**: load_config/save_config pattern is not truly atomic (race condition possible if multiple processes access config). Acceptable for CLI tool; document as limitation if needed.

### Dependency Risks
- [x] Subtask 4.1 completed - no blockers

### Open Questions
- [ ] Should `set_tenant` validate that `profile.name` matches the `name` parameter, or silently update `profile.name`? Task details say "Ensure the profile.name matches the key being set" - suggest raising ValidationError if mismatch.

---

## Implementation Recommendations

### Suggested Approach
1. Add `NotFoundError` import to config_manager.py
2. Implement `get_tenant(name: str) -> TenantProfile`:
   - Call `load_config()`
   - Check if `name` in `config.tenants`
   - If not found, raise `NotFoundError` with helpful message
   - Return `config.tenants[name]`
3. Implement `set_tenant(name: str, profile: TenantProfile)`:
   - Validate `name == profile.name` (raise ValidationError if mismatch)
   - Call `load_config()`
   - Set `config.tenants[name] = profile`
   - Call `save_config(config)`
4. Implement `remove_tenant(name: str)`:
   - Call `load_config()`
   - Check if `name` in `config.tenants`, raise `NotFoundError` if not
   - Delete `config.tenants[name]`
   - If `config.default_tenant == name`, set `config.default_tenant = None`
   - Call `save_config(config)`
5. Write unit tests for each method including error cases

### Files to Create/Modify

| Action | File | Reason |
|--------|------|--------|
| Modify | `src/teams_phone/infrastructure/config_manager.py` | Add get_tenant, set_tenant, remove_tenant methods |
| Modify | `tests/unit/test_config_manager.py` | Add test classes for tenant CRUD operations |

### Quick Wins
- Implement `get_tenant` first - simplest method, validates load_config integration
- Reuse TenantProfile creation pattern from existing tests (`test_config_manager.py:230-237`)

---

## Cheat Sheet

### Key Imports
```python
# Already in config_manager.py, just need to add NotFoundError:
from teams_phone.exceptions import ConfigurationError, NotFoundError

# For validation in set_tenant (optional, if validating name match):
from teams_phone.exceptions import ValidationError
```

### Key Functions to Use
- `self.load_config() -> Config` - loads and validates config from TOML file
- `self.save_config(config: Config) -> None` - saves config to TOML file
- `config.tenants` - `dict[str, TenantProfile]` for tenant storage
- `config.default_tenant` - `str | None` for default tenant name

### Common Patterns
- **Load-modify-save pattern**: `config = self.load_config(); config.tenants[name] = profile; self.save_config(config)`
- **NotFoundError pattern**: `raise NotFoundError(f"Tenant '{name}' not found", remediation=f"Run 'tenants list' to see configured tenants")`
- **Dict key check**: `if name not in config.tenants: raise NotFoundError(...)`

### Method Signatures (from task details)
```python
def get_tenant(self, name: str) -> TenantProfile: ...
def set_tenant(self, name: str, profile: TenantProfile) -> None: ...
def remove_tenant(self, name: str) -> None: ...
```

---

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