feat(backend): add Item model and stage/type validation

This commit is contained in:
pyr0ball 2026-07-13 13:33:40 -07:00
parent 337c2a60d1
commit c3e9f2551c
9 changed files with 122 additions and 0 deletions

11
backend/.gitignore vendored Normal file
View file

@ -0,0 +1,11 @@
venv/
__pycache__/
.pytest_cache/
*.pyc
*.pyo
*.egg-info/
dist/
build/
.env
.env.local
*.db

0
backend/app/__init__.py Normal file
View file

19
backend/app/db.py Normal file
View file

@ -0,0 +1,19 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, DeclarativeBase
class Base(DeclarativeBase):
pass
def get_engine(db_url: str):
connect_args = {"check_same_thread": False} if db_url.startswith("sqlite") else {}
return create_engine(db_url, connect_args=connect_args)
def make_session_factory(engine):
return sessionmaker(bind=engine, autoflush=False, autocommit=False)
def init_db(engine):
Base.metadata.create_all(bind=engine)

23
backend/app/models.py Normal file
View file

@ -0,0 +1,23 @@
from datetime import datetime, date
from sqlalchemy import String, Text, DateTime, Date, Integer
from sqlalchemy.orm import Mapped, mapped_column
from app.db import Base
class Item(Base):
__tablename__ = "items"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
modality: Mapped[str] = mapped_column(String, nullable=False)
raw_content: Mapped[str] = mapped_column(Text, nullable=False)
captured_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
sender_id: Mapped[str | None] = mapped_column(String, nullable=True)
type: Mapped[str | None] = mapped_column(String, nullable=True)
stage: Mapped[str] = mapped_column(String, nullable=False, default="new")
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
follow_up_date: Mapped[date | None] = mapped_column(Date, nullable=True)
discord_message_id: Mapped[str | None] = mapped_column(String, unique=True, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
)

14
backend/app/stages.py Normal file
View file

@ -0,0 +1,14 @@
DONATION_STAGES = ["new", "replied", "pickup_scheduled", "picked_up", "logged_in_bookmark"]
OTHER_STAGES = ["new", "done"]
def valid_stages_for(item_type: str | None) -> list[str]:
if item_type == "donation":
return DONATION_STAGES
if item_type == "other":
return OTHER_STAGES
return ["new"]
def is_valid_stage(item_type: str | None, stage: str) -> bool:
return stage in valid_stages_for(item_type)

6
backend/requirements.txt Normal file
View file

@ -0,0 +1,6 @@
fastapi==0.115.0
uvicorn[standard]==0.30.6
sqlalchemy==2.0.35
pydantic==2.9.2
pytest==8.3.3
httpx==0.27.2

View file

14
backend/tests/conftest.py Normal file
View file

@ -0,0 +1,14 @@
import pytest
from app.db import get_engine, make_session_factory, init_db
@pytest.fixture()
def db_session():
engine = get_engine("sqlite:///:memory:")
init_db(engine)
SessionLocal = make_session_factory(engine)
session = SessionLocal()
try:
yield session
finally:
session.close()

View file

@ -0,0 +1,35 @@
from app import stages
def test_donation_stages_in_order():
assert stages.DONATION_STAGES == [
"new", "replied", "pickup_scheduled", "picked_up", "logged_in_bookmark",
]
def test_other_stages_in_order():
assert stages.OTHER_STAGES == ["new", "done"]
def test_valid_stages_for_donation():
assert stages.valid_stages_for("donation") == stages.DONATION_STAGES
def test_valid_stages_for_other():
assert stages.valid_stages_for("other") == stages.OTHER_STAGES
def test_valid_stages_for_unset_type_defaults_to_new_only():
assert stages.valid_stages_for(None) == ["new"]
def test_is_valid_stage_true_for_matching_type():
assert stages.is_valid_stage("donation", "pickup_scheduled") is True
def test_is_valid_stage_false_for_mismatched_type():
assert stages.is_valid_stage("other", "pickup_scheduled") is False
def test_is_valid_stage_false_for_unknown_value():
assert stages.is_valid_stage("donation", "archived") is False