---
phase: 01-foundation
plan: 02
type: execute
wave: 2
depends_on:
  - 01-01
files_modified:
  - possession/models.py
  - tests/test_models.py
autonomous: true
requirements:
  - DATA-01
  - DATA-02
  - DATA-03
  - DATA-04

must_haves:
  truths:
    - "Rooms can be created, renamed, and deleted without data loss to unrelated data"
    - "Containers can be created, renamed, and deleted within their parent room"
    - "Deleting a room cascades to delete its containers"
    - "Items can be created with all 7 fields (name, description, room, container, category, purchase_date, cost)"
    - "Categories can be added, renamed, and deleted"
    - "Deleting a category sets items' category_id to NULL (no orphan constraint violation)"
  artifacts:
    - path: "possession/models.py"
      provides: "CRUD functions for rooms, containers, items, categories"
      exports:
        - "create_room, rename_room, delete_room, list_rooms"
        - "create_container, rename_container, delete_container, list_containers"
        - "create_item, list_items"
        - "create_category, rename_category, delete_category, list_categories"
    - path: "tests/test_models.py"
      provides: "Pytest tests for all CRUD operations"
      contains: "def test_"
  key_links:
    - from: "possession/models.py"
      to: "possession/db.py"
      via: "calls get_connection(path) for each operation"
      pattern: "get_connection"
    - from: "tests/test_models.py"
      to: "possession/models.py"
      via: "imports and calls all CRUD functions"
      pattern: "from possession.models import"
---

<objective>
Implement all CRUD data model operations for the four entities: rooms, containers, items, and categories. Each entity gets create, rename/update, delete, and list functions backed by the SQLite schema from Plan 01-01.

Purpose: These functions are the data layer that Phase 2 (TUI browse) and Phase 3 (quick-add, edit, delete) will call directly. Getting them right here prevents rework later.
Output: `possession/models.py` with clean functions + `tests/test_models.py` verifying all operations.
</objective>

<execution_context>
@/Users/paritoshpatil/.claude/get-shit-done/workflows/execute-plan.md
@/Users/paritoshpatil/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/01-foundation/01-01-SUMMARY.md
</context>

<tasks>

<task type="auto">
  <name>Task 1: CRUD operations for rooms, containers, categories, and items</name>
  <files>
    possession/models.py
  </files>
  <action>
Create `possession/models.py` with CRUD functions for all four entities. Each function accepts a `db_path: Path` as its first parameter (use `get_connection` from `possession.db` internally — open, operate, commit/close).

**Rooms** (DATA-01):
```python
def create_room(db_path: Path, name: str) -> int:
    # INSERT INTO rooms (name) VALUES (?)
    # Returns new row id. Raises sqlite3.IntegrityError on duplicate name.

def rename_room(db_path: Path, room_id: int, new_name: str) -> None:
    # UPDATE rooms SET name=? WHERE id=?
    # Raises ValueError if room_id not found.

def delete_room(db_path: Path, room_id: int) -> None:
    # DELETE FROM rooms WHERE id=?
    # Cascades to containers (ON DELETE CASCADE in schema).
    # Raises ValueError if room_id not found.

def list_rooms(db_path: Path) -> list[dict]:
    # SELECT id, name, created_at FROM rooms ORDER BY name
    # Returns list of dicts with keys: id, name, created_at
```

**Containers** (DATA-02):
```python
def create_container(db_path: Path, name: str, room_id: int) -> int:
    # INSERT INTO containers (name, room_id) VALUES (?, ?)
    # Returns new row id. Raises sqlite3.IntegrityError on duplicate (name, room_id).

def rename_container(db_path: Path, container_id: int, new_name: str) -> None:
    # UPDATE containers SET name=? WHERE id=?
    # Raises ValueError if not found.

def delete_container(db_path: Path, container_id: int) -> None:
    # DELETE FROM containers WHERE id=?
    # Raises ValueError if not found.

def list_containers(db_path: Path, room_id: int | None = None) -> list[dict]:
    # If room_id given: SELECT ... WHERE room_id=?
    # Else: SELECT all containers
    # Returns list of dicts: id, name, room_id, created_at
```

**Categories** (DATA-04):
```python
def create_category(db_path: Path, name: str) -> int:
    # INSERT INTO categories (name) VALUES (?)
    # Returns new row id.

def rename_category(db_path: Path, category_id: int, new_name: str) -> None:
    # UPDATE categories SET name=? WHERE id=?

def delete_category(db_path: Path, category_id: int) -> None:
    # DELETE FROM categories WHERE id=?
    # Items with this category get category_id=NULL (ON DELETE SET NULL in schema).

def list_categories(db_path: Path) -> list[dict]:
    # SELECT id, name, created_at FROM categories ORDER BY name
```

**Items** (DATA-03):
```python
def create_item(
    db_path: Path,
    name: str,
    description: str | None = None,
    room_id: int | None = None,
    container_id: int | None = None,
    category_id: int | None = None,
    purchase_date: str | None = None,
    cost: float | None = None,
) -> int:
    # INSERT INTO items (name, description, room_id, container_id, category_id, purchase_date, cost)
    # Returns new row id. All fields except name are optional (NULL allowed).

def list_items(
    db_path: Path,
    room_id: int | None = None,
    container_id: int | None = None,
    category_id: int | None = None,
) -> list[dict]:
    # SELECT items.*, rooms.name as room_name, containers.name as container_name,
    #        categories.name as category_name
    # FROM items
    # LEFT JOIN rooms ON items.room_id = rooms.id
    # LEFT JOIN containers ON items.container_id = containers.id
    # LEFT JOIN categories ON items.category_id = categories.id
    # Filter by room_id / container_id / category_id if provided
    # Returns list of dicts with all columns including joined names
```

Use `conn.row_factory = sqlite3.Row` before queries to get dict-like rows. Convert to plain dicts with `dict(row)` before returning. Enable foreign keys with `PRAGMA foreign_keys=ON` in `get_connection` (already done in db.py — confirm it's present, don't add again).

No ORM. No dataclasses required. Plain dicts are sufficient for Phase 2.
  </action>
  <verify>
    <automated>cd /Users/paritoshpatil/Personal/claude-projects/possession-terminal-2 && python -c "
import tempfile, os
from pathlib import Path
from possession.db import init_db
from possession.models import (
    create_room, rename_room, delete_room, list_rooms,
    create_container, rename_container, delete_container, list_containers,
    create_category, rename_category, delete_category, list_categories,
    create_item, list_items,
)
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
    p = Path(f.name)
try:
    init_db(str(p))
    # Rooms
    rid = create_room(p, 'Garage')
    rename_room(p, rid, 'Workshop')
    assert list_rooms(p)[0]['name'] == 'Workshop', 'rename failed'
    # Containers
    cid = create_container(p, 'Tool Cabinet', rid)
    rename_container(p, cid, 'Big Cabinet')
    assert list_containers(p, rid)[0]['name'] == 'Big Cabinet'
    # Categories
    catid = create_category(p, 'Tools')
    rename_category(p, catid, 'Hand Tools')
    assert list_categories(p)[0]['name'] == 'Hand Tools'
    # Items (all 7 fields)
    iid = create_item(p, 'Hammer', 'Heavy', rid, cid, catid, '2024-01-01', 29.99)
    items = list_items(p)
    assert len(items) == 1
    assert items[0]['name'] == 'Hammer'
    assert items[0]['room_name'] == 'Workshop'
    # Cascade: delete room cascades to container
    delete_room(p, rid)
    assert list_containers(p) == []
    print('PASS: all CRUD operations verified')
finally:
    os.unlink(p)
" 2>&1</automated>
  </verify>
  <done>
    - All four entity types have working create, rename/update, delete, and list functions
    - `list_items` returns joined room_name, container_name, category_name fields
    - All functions accept `db_path: Path` as first argument
    - Deleting a room cascades to its containers
    - Deleting a category sets items' category_id to NULL (no error)
  </done>
</task>

<task type="auto">
  <name>Task 2: Pytest test suite for all data model operations</name>
  <files>
    tests/__init__.py
    tests/test_models.py
  </files>
  <action>
Create `tests/__init__.py` (empty) and `tests/test_models.py` with pytest tests covering all CRUD operations:

Use a pytest fixture to create a fresh temporary database for each test:

```python
import pytest
import tempfile
import os
from pathlib import Path
from possession.db import init_db
from possession.models import (
    create_room, rename_room, delete_room, list_rooms,
    create_container, rename_container, delete_container, list_containers,
    create_category, rename_category, delete_category, list_categories,
    create_item, list_items,
)

@pytest.fixture
def db_path(tmp_path):
    p = tmp_path / "test.db"
    init_db(str(p))
    return p
```

Write at minimum these test functions (one assertion per test for clear failure messages):

**Room tests:**
- `test_create_room` — creates room, appears in list_rooms
- `test_rename_room` — rename changes name in list
- `test_delete_room` — delete removes from list
- `test_delete_room_cascades_containers` — deleting room removes its containers

**Container tests:**
- `test_create_container` — creates container in room, appears in list_containers
- `test_rename_container` — rename reflects in list
- `test_delete_container` — removes from list
- `test_list_containers_filtered_by_room` — list_containers(room_id=X) only returns that room's containers

**Category tests:**
- `test_create_category` — appears in list_categories
- `test_rename_category` — name updated
- `test_delete_category_nulls_items` — after deleting category, item's category_id is NULL (not an error)

**Item tests:**
- `test_create_item_all_fields` — all 7 fields stored and retrievable
- `test_create_item_name_only` — partial create (name only) succeeds
- `test_list_items_includes_joined_names` — room_name, container_name, category_name present in result
- `test_list_items_filtered_by_room` — list_items(room_id=X) returns only that room's items

Do NOT use mocks. Test against real SQLite in-memory temp files (the fixture handles this). Keep tests < 10 lines each.
  </action>
  <verify>
    <automated>cd /Users/paritoshpatil/Personal/claude-projects/possession-terminal-2 && python -m pytest tests/test_models.py -v 2>&1 | tail -20</automated>
  </verify>
  <done>
    - `pytest tests/test_models.py` passes all tests with 0 failures
    - At minimum 15 test functions covering all 4 entities
    - Tests use temp files (no side effects on real DB)
  </done>
</task>

</tasks>

<verification>
```bash
cd /Users/paritoshpatil/Personal/claude-projects/possession-terminal-2
python -m pytest tests/test_models.py -v
# Expect: all tests pass (green)

# Verify no hardcoded paths
grep -r "possession.db" possession/models.py && echo "WARNING: hardcoded path found" || echo "OK: no hardcoded paths"
```
</verification>

<success_criteria>
- `pytest tests/test_models.py` passes all tests with 0 failures, 0 errors
- All 4 entities have create, rename/update, delete, and list functions in `possession/models.py`
- `list_items()` returns dicts with `room_name`, `container_name`, `category_name` from JOINs
- Cascade behavior verified: deleting a room removes its containers; deleting a category nulls item references
- No hardcoded database paths in models.py
</success_criteria>

<output>
After completion, create `.planning/phases/01-foundation/01-02-SUMMARY.md` documenting:
- All functions implemented and their signatures
- Cascade behavior confirmed (room→containers cascade, category→items set null)
- Test coverage count and any edge cases found
- Any deviations from this plan
</output>
