← back to Unclaimed Property Platform
services/claims/sqlite_claim_repo.py
134 lines
"""Concrete SQLite ClaimRepository — makes the claim subsystem runnable end-to-end.
Implements the ClaimRepository protocol from claim_workflow against the schema's
claim_case / claim_event / outbox_event tables, sharing the same connection as
SqliteRepository (one system-of-record DB). Production is the PostgreSQL equivalent with
real SELECT ... FOR UPDATE row locks; here the prototype is single-threaded so the
optimistic version field + the UNIQUE(claim_id, idempotency_key) constraint carry integrity.
Crucially the DB — not a mock — enforces idempotency: a duplicate (claim_id, idempotency_key)
raises sqlite3.IntegrityError, so the C3 fix is proven against real constraints.
"""
from __future__ import annotations
import json
import sqlite3
import uuid
from datetime import datetime, timezone
from services.claims.claim_workflow import Claim, ClaimStatus
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
class SqliteClaimRepository:
def __init__(self, conn: sqlite3.Connection) -> None:
self.conn = conn
self.conn.row_factory = sqlite3.Row
def create_claim(self, jurisdiction: str, public_property_reference: str,
claimant_id: uuid.UUID) -> Claim:
claim = Claim(
claim_id=uuid.uuid4(), jurisdiction=jurisdiction,
public_property_reference=public_property_reference,
claimant_id=claimant_id, status=ClaimStatus.DRAFT, version=1,
)
self.conn.execute(
"""INSERT INTO claim_case
(claim_id, jurisdiction_id, public_property_reference, claimant_id,
status, version, state_case_id)
VALUES (?,?,?,?,?,?,?)""",
(str(claim.claim_id), claim.jurisdiction, claim.public_property_reference,
str(claim.claimant_id), claim.status.value, claim.version, None),
)
self.conn.commit()
return claim
# --- ClaimRepository protocol -----------------------------------------
def get_for_update(self, claim_id: uuid.UUID) -> Claim:
row = self.conn.execute(
"SELECT * FROM claim_case WHERE claim_id=?", (str(claim_id),)
).fetchone()
if row is None:
raise KeyError(f"claim {claim_id} not found")
return Claim(
claim_id=uuid.UUID(row["claim_id"]),
jurisdiction=row["jurisdiction_id"],
public_property_reference=row["public_property_reference"],
claimant_id=uuid.UUID(row["claimant_id"]),
status=ClaimStatus(row["status"]),
version=row["version"],
state_case_id=row["state_case_id"],
)
def save(self, claim: Claim) -> None:
self.conn.execute(
"UPDATE claim_case SET status=?, version=?, state_case_id=? WHERE claim_id=?",
(claim.status.value, claim.version, claim.state_case_id, str(claim.claim_id)),
)
self.conn.commit()
def append_event(self, claim_id: uuid.UUID, event_type: str, payload: dict,
idempotency_key: str) -> None:
# UNIQUE(claim_id, idempotency_key) is enforced by the DB — a collision raises
# sqlite3.IntegrityError (the real C3 guard, not a mock's imitation).
self.conn.execute(
"""INSERT INTO claim_event
(claim_event_id, claim_id, event_type, payload, idempotency_key, created_at)
VALUES (?,?,?,?,?,?)""",
(str(uuid.uuid4()), str(claim_id), event_type, json.dumps(payload),
idempotency_key, _now()),
)
self.conn.commit()
def add_outbox_event(self, event_type: str, aggregate_id: uuid.UUID, payload: dict) -> None:
self.conn.execute(
"""INSERT INTO outbox_event
(outbox_event_id, aggregate_id, event_type, payload, delivery_state)
VALUES (?,?,?,?, 'pending')""",
(str(uuid.uuid4()), str(aggregate_id), event_type, json.dumps(payload)),
)
self.conn.commit()
# --- reads for tests / workers ----------------------------------------
def events(self, claim_id: uuid.UUID) -> list[dict]:
rows = self.conn.execute(
"SELECT event_type, idempotency_key FROM claim_event WHERE claim_id=? ORDER BY created_at",
(str(claim_id),),
).fetchall()
return [dict(r) for r in rows]
def pending_outbox(self, aggregate_id: uuid.UUID) -> list[dict]:
rows = self.conn.execute(
"SELECT event_type, delivery_state FROM outbox_event WHERE aggregate_id=?",
(str(aggregate_id),),
).fetchall()
return [dict(r) for r in rows]
# --- outbox worker surface --------------------------------------------
def fetch_pending_outbox(self, limit: int = 100) -> list[dict]:
rows = self.conn.execute(
"""SELECT outbox_event_id, aggregate_id, event_type, payload, attempts
FROM outbox_event WHERE delivery_state='pending'
ORDER BY rowid LIMIT ?""",
(limit,),
).fetchall()
return [dict(r) for r in rows]
def mark_outbox(self, outbox_event_id: str, delivery_state: str,
attempts: int, last_error: str | None = None) -> None:
self.conn.execute(
"UPDATE outbox_event SET delivery_state=?, attempts=?, last_error=? WHERE outbox_event_id=?",
(delivery_state, attempts, last_error, outbox_event_id),
)
self.conn.commit()
def outbox_row(self, outbox_event_id: str) -> dict | None:
r = self.conn.execute(
"SELECT delivery_state, attempts, last_error FROM outbox_event WHERE outbox_event_id=?",
(outbox_event_id,),
).fetchone()
return dict(r) if r else None