# Transactions Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add transaction support with callback (auto-retry) and context manager (single-attempt) patterns, plus `Txn`/`AsyncTxn` wrappers with read and write methods.

**Architecture:** `Txn`/`AsyncTxn` in new `transaction.py`, wrapping Firestore's `Transaction`/`AsyncTransaction`. Write methods mirror `Batch`. Read methods use `doc_ref.get(transaction=self._transaction)`. Context methods use Firestore's `@transactional`/`@async_transactional` for callback pattern.

**Tech Stack:** Python 3.13+, google-cloud-firestore, pytest, anyio

**Spec:** `docs/superpowers/specs/2026-03-22-transactions-design.md`

---

## File Map

| File | Action | Responsibility |
|------|--------|---------------|
| `src/cendry/transaction.py` | Create | `Txn`, `AsyncTxn` wrapper classes |
| `src/cendry/context.py` | Modify | Add `transaction()` to both context classes |
| `src/cendry/__init__.py` | Modify | Export `Txn`, `AsyncTxn` |
| `tests/test_transaction.py` | Create | Tests for `Txn`/`AsyncTxn` methods |
| `tests/test_context.py` | Modify | Tests for `ctx.transaction()` callback and context manager |
| `tests/test_public_api.py` | Modify | Add exports |

---

### Task 1: `Txn` and `AsyncTxn` classes

**Files:**
- Create: `src/cendry/transaction.py`
- Create: `tests/test_transaction.py`

The `Txn` class has the same write methods as `Batch` plus `get`/`find` read methods. The key difference is that reads call `doc_ref.get(transaction=self._transaction)` and the context manager calls `_begin()`/`_commit()`/`_rollback()`.

- [ ] **Step 1: Write tests and implementation together** (large new file — TDD at the class level)

Create `tests/test_transaction.py` with tests for all `Txn` methods:
- `test_txn_get_returns_model` — reads via transaction, deserializes
- `test_txn_get_missing_raises` — `DocumentNotFoundError`
- `test_txn_find_returns_model` — find semantics
- `test_txn_find_missing_returns_none`
- `test_txn_save_explicit_id` — queues set
- `test_txn_save_auto_id` — mutates instance.id
- `test_txn_create` — queues create
- `test_txn_update_by_instance` — queues update
- `test_txn_update_by_class_and_id`
- `test_txn_update_no_id_raises`
- `test_txn_delete_by_instance` — queues delete
- `test_txn_delete_by_class_and_id`
- `test_txn_delete_no_id_raises`
- `test_txn_context_manager_commits`
- `test_txn_context_manager_rollback_on_exception`
- Async mirrors for all of the above

Create `src/cendry/transaction.py` with `Txn` and `AsyncTxn`.

`Txn` structure:
- Constructor: `(firestore_transaction, get_collection_ref, registry)`
- `__enter__`: calls `self._transaction._begin()`, returns self
- `__exit__`: commits on success, rolls back on exception
- `get(model_class, doc_id, *, parent)`: reads via `doc_ref.get(transaction=self._transaction)`, deserializes
- `find(model_class, doc_id, *, parent)`: same but returns None if not found
- Write methods: identical to `Batch` but call `self._transaction.set/create/update/delete` instead of `self._batch.set/create/update/delete`

`AsyncTxn` structure:
- Same as `Txn` but `__aenter__`/`__aexit__` are async, `get`/`find` are async
- Write methods are sync (they queue, don't execute)

- [ ] **Step 2: Run tests**

Run: `uv run pytest tests/test_transaction.py -v`
Expected: All pass

- [ ] **Step 3: Run full test suite**

Run: `uv run pytest -q`
Expected: All pass

- [ ] **Step 4: Commit**

```bash
git add src/cendry/transaction.py tests/test_transaction.py
git commit -m "feat: add Txn and AsyncTxn transaction wrapper classes"
```

---

### Task 2: Context methods and exports

**Files:**
- Modify: `src/cendry/context.py`
- Modify: `src/cendry/__init__.py`
- Modify: `tests/test_context.py`
- Modify: `tests/test_public_api.py`

- [ ] **Step 1: Write tests**

Add to `tests/test_context.py`:
- `test_transaction_callback` — `ctx.transaction(fn)` executes fn
- `test_transaction_context_manager` — `with ctx.transaction() as txn:` works
- `test_transaction_context_manager_commits` — commits on exit
- `test_async_transaction_callback` — async version
- `test_async_transaction_context_manager` — async version

Add to `tests/test_public_api.py`: `"AsyncTxn"` and `"Txn"` in expected list.

- [ ] **Step 2: Implement context methods**

Add to `Cendry` (after `delete_many`):

```python
@overload
def transaction(
    self,
    fn: Callable[[Txn], Any],
    *,
    max_attempts: int = 5,
    read_only: bool = False,
) -> Any: ...
@overload
def transaction(
    self,
    fn: None = None,
    *,
    max_attempts: int = 5,
    read_only: bool = False,
) -> Txn: ...

def transaction(self, fn=None, *, max_attempts=5, read_only=False):
    from .transaction import Txn
    from google.cloud.firestore_v1.transaction import transactional

    fs_txn = self._client.transaction(max_attempts=max_attempts, read_only=read_only)
    txn = Txn(fs_txn, self._get_collection_ref, self.type_registry)
    if fn is None:
        return txn

    @transactional
    def _run(transaction):
        return fn(txn)

    return _run(fs_txn)
```

Add equivalent to `AsyncCendry` using `async_transactional`.

Add exports to `__init__.py`: `from .transaction import AsyncTxn, Txn`

- [ ] **Step 3: Run tests**

Run: `uv run pytest tests/test_context.py tests/test_public_api.py -q`
Expected: All pass

- [ ] **Step 4: Commit**

```bash
git add src/cendry/context.py src/cendry/__init__.py tests/test_context.py tests/test_public_api.py
git commit -m "feat: add transaction() to Cendry/AsyncCendry and exports"
```

---

### Task 3: Type checking, linting, and final verification

- [ ] **Step 1:** `uv run ruff check --fix src/ tests/ && uv run ruff format src/ tests/`
- [ ] **Step 2:** `uv run mypy src/cendry/ tests/`
- [ ] **Step 3:** `uv run ty check src/cendry/ tests/`
- [ ] **Step 4:** `uv run coverage run -m pytest && uv run coverage report --show-missing` — must be 100%
- [ ] **Step 5:** Fix issues and commit
