feat(meals): enforce three lower-calorie dinners with media
CI / backend (pytest + alembic) (push) Canceled after 0s
CI / frontend (build) (push) Canceled after 0s

This commit is contained in:
2026-09-01 10:11:36 -07:00
parent 7838c49721
commit 2695ba0232
85 changed files with 8103 additions and 795 deletions
@@ -0,0 +1,120 @@
"""Add global recipe import staging and provenance storage.
Revision ID: 0017
Revises: 0016
Create Date: 2026-08-14
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = "0017"
down_revision: Union[str, None] = "0016"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
recipe_import_status_enum = postgresql.ENUM(
"pending",
"quarantined",
"approved",
"rejected",
name="recipe_import_status_enum",
create_type=False,
)
def upgrade() -> None:
recipe_import_status_enum.create(op.get_bind(), checkfirst=True)
op.create_table(
"recipe_import_staging",
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("source", sa.String(length=100), nullable=False),
sa.Column("external_id", sa.String(length=255), nullable=False),
sa.Column("source_url", sa.Text(), nullable=False),
sa.Column("source_revision", sa.String(length=255), nullable=False),
sa.Column("license_name", sa.String(length=255), nullable=False),
sa.Column("license_url", sa.Text(), nullable=False),
sa.Column("attribution", sa.Text(), nullable=False),
sa.Column("raw_payload", postgresql.JSONB(), nullable=False),
sa.Column("payload_sha256", sa.String(length=64), nullable=False),
sa.Column("normalized_recipe", postgresql.JSONB(), nullable=False),
sa.Column("validation_report", postgresql.JSONB(), nullable=False),
sa.Column(
"status",
recipe_import_status_enum,
nullable=False,
server_default="pending",
),
sa.Column("rejection_reason", sa.Text(), nullable=True),
sa.Column("recipe_id", postgresql.UUID(as_uuid=True), nullable=True),
sa.Column(
"acquired_at",
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.text("now()"),
),
sa.Column("reviewed_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("reviewed_by", sa.String(length=300), nullable=True),
sa.CheckConstraint(
"payload_sha256 ~ '^[0-9a-f]{64}$'",
name="ck_recipe_import_staging_payload_sha256",
),
sa.CheckConstraint(
"(reviewed_at IS NULL) = (reviewed_by IS NULL)",
name="ck_recipe_import_staging_review_metadata",
),
sa.ForeignKeyConstraint(["recipe_id"], ["recipe.id"], ondelete="SET NULL"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint(
"source",
"external_id",
"source_revision",
name="uq_recipe_import_staging_source_revision",
),
)
op.create_index(
"ix_recipe_import_staging_status_acquired_at",
"recipe_import_staging",
["status", "acquired_at"],
unique=False,
)
op.execute(
"""
CREATE FUNCTION prevent_recipe_import_staging_raw_payload_update()
RETURNS trigger AS $$
BEGIN
IF NEW.raw_payload IS DISTINCT FROM OLD.raw_payload THEN
RAISE EXCEPTION 'recipe_import_staging.raw_payload is immutable';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql
"""
)
op.execute(
"""
CREATE TRIGGER trg_recipe_import_staging_raw_payload_immutable
BEFORE UPDATE OF raw_payload ON recipe_import_staging
FOR EACH ROW
EXECUTE FUNCTION prevent_recipe_import_staging_raw_payload_update()
"""
)
def downgrade() -> None:
op.execute(
"DROP TRIGGER trg_recipe_import_staging_raw_payload_immutable "
"ON recipe_import_staging"
)
op.execute("DROP FUNCTION prevent_recipe_import_staging_raw_payload_update()")
op.drop_index(
"ix_recipe_import_staging_status_acquired_at",
table_name="recipe_import_staging",
)
op.drop_table("recipe_import_staging")
recipe_import_status_enum.drop(op.get_bind(), checkfirst=True)
@@ -0,0 +1,65 @@
"""Add recipe catalog quarantine state.
Revision ID: 0018
Revises: 0017
Create Date: 2026-08-14
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision: str = "0018"
down_revision: Union[str, None] = "0017"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
recipe_catalog_status_enum = postgresql.ENUM(
"active",
"quarantined",
name="recipe_catalog_status_enum",
create_type=False,
)
def upgrade() -> None:
recipe_catalog_status_enum.create(op.get_bind(), checkfirst=True)
op.add_column(
"recipe",
sa.Column(
"catalog_status",
recipe_catalog_status_enum,
nullable=False,
server_default="active",
),
)
op.add_column(
"recipe",
sa.Column(
"catalog_findings",
postgresql.JSONB(),
nullable=False,
server_default=sa.text("'[]'::jsonb"),
),
)
op.add_column(
"recipe",
sa.Column("catalog_reviewed_at", sa.DateTime(timezone=True), nullable=True),
)
op.create_index(
"ix_recipe_catalog_status",
"recipe",
["catalog_status"],
unique=False,
)
def downgrade() -> None:
op.drop_index("ix_recipe_catalog_status", table_name="recipe")
op.drop_column("recipe", "catalog_reviewed_at")
op.drop_column("recipe", "catalog_findings")
op.drop_column("recipe", "catalog_status")
recipe_catalog_status_enum.drop(op.get_bind(), checkfirst=True)