From c3e7a14c9c82d86cbe01cd23d017e4f9990ae24b Mon Sep 17 00:00:00 2001 From: "cartsnitch-ci[bot]" Date: Tue, 31 Mar 2026 01:09:16 +0000 Subject: [PATCH 1/4] fix(api): correct COPY paths in Dockerfile for monorepo build context The api/Dockerfile used bare paths (COPY pyproject.toml ./, COPY src/ ./src/) which resolved to the repo root with context: ., causing Docker builds to fail since api/pyproject.toml and api/src/ don't exist at the repo root. Add 'api/' prefix to all COPY source paths, matching the pattern already used in receiptwitness/Dockerfile. Co-Authored-By: Paperclip --- Dockerfile | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8eef88d..0c5b700 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,5 @@ +# Stage 1: Build dependencies +# Build context is the repo root. Paths below are relative to the root. FROM python:3.12-slim AS build RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -6,18 +8,19 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* WORKDIR /app -COPY pyproject.toml ./ -COPY src/ ./src/ +COPY api/pyproject.toml ./ +COPY api/src/ ./src/ RUN pip install --no-cache-dir --prefix=/install . +# Stage 2: Production image FROM python:3.12-slim AS prod WORKDIR /app RUN adduser --system --group --uid 1000 app COPY --from=build /install /usr/local -COPY src/ ./src/ -COPY alembic.ini ./ -COPY alembic/ ./alembic/ +COPY api/src/ ./src/ +COPY api/alembic.ini ./ +COPY api/alembic/ ./alembic/ USER 1000 EXPOSE 8000 From 93cfaf55f79a7f26341b7e0dea8b8433e8bf91bd Mon Sep 17 00:00:00 2001 From: "cartsnitch-engineer[bot]" <269717931+cartsnitch-engineer[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 02:48:25 +0000 Subject: [PATCH 2/4] fix(api): add libpq5 to prod stage for psycopg2 runtime Co-Authored-By: Paperclip --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index 0c5b700..e271e94 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,8 @@ RUN pip install --no-cache-dir --prefix=/install . # Stage 2: Production image FROM python:3.12-slim AS prod +RUN apt-get update && apt-get install -y --no-install-recommends libpq5 && rm -rf /var/lib/apt/lists/* + WORKDIR /app RUN adduser --system --group --uid 1000 app COPY --from=build /install /usr/local From a1d53f8e47e92b6556339fd3c401bc98bf8cc970 Mon Sep 17 00:00:00 2001 From: Stockboy Steve Date: Tue, 31 Mar 2026 17:56:13 +0000 Subject: [PATCH 3/4] fix: change users.id and FK columns from uuid to text for Better-Auth compatibility Better-Auth generates nanoid-style text IDs (e.g. pGud2ln2WAFHC0KYjBVKR4Rc7mM8OcTI), but the users table used PostgreSQL uuid type, causing registration failures: ERROR: invalid input syntax for type uuid: "pGud2ln2WAFHC0KYjBVKR4Rc7mM8OcTI" Changes: - User.id: removed UUIDPrimaryKeyMixin, use explicit text PK - UserStoreAccount.user_id: Mapped[uuid.UUID] -> Mapped[str] - Purchase.user_id: Mapped[uuid.UUID] -> Mapped[str] - UserResponse schema: id field from UUID -> str - New Alembic migration 004_fix_user_id_text: drops FKs, alters column types, re-adds FKs (using id::text cast) Co-Authored-By: Paperclip --- alembic/versions/004_fix_user_id_text.py | 122 +++++++++++++++++++++++ src/cartsnitch_api/models/purchase.py | 2 +- src/cartsnitch_api/models/user.py | 7 +- src/cartsnitch_api/schemas.py | 2 +- 4 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 alembic/versions/004_fix_user_id_text.py diff --git a/alembic/versions/004_fix_user_id_text.py b/alembic/versions/004_fix_user_id_text.py new file mode 100644 index 0000000..a52bf9d --- /dev/null +++ b/alembic/versions/004_fix_user_id_text.py @@ -0,0 +1,122 @@ +"""Fix users.id UUID->text type mismatch for Better-Auth compatibility. + +Better-Auth generates nanoid-style text IDs (e.g. pGud2ln2WAFHC0KYjBVKR4Rc7mM8OcTI), +but the users table was using PostgreSQL uuid type. When Better-Auth tries to INSERT +a new user, Postgres throws: + ERROR: invalid input syntax for type uuid: "pGud2ln2WAFHC0KYjBVKR4Rc7mM8OcTI" + +The sessions, accounts, and verifications tables already use text IDs — only users, +user_store_accounts.user_id, and purchases.user_id needed fixing. + +Revision ID: 004_fix_user_id_text +Revises: 003_make_users_hashed_password_nullable +Create Date: 2026-03-31 +""" + +import sqlalchemy as sa +from sqlalchemy import text + +from alembic import op + +revision = "004_fix_user_id_text" +down_revision = "003_make_users_hashed_password_nullable" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Step 1: Drop existing FK constraints + op.execute(text("ALTER TABLE user_store_accounts DROP CONSTRAINT IF EXISTS user_store_accounts_user_id_fkey")) + op.execute(text("ALTER TABLE purchases DROP CONSTRAINT IF EXISTS purchases_user_id_fkey")) + + # Step 2: Alter users.id from uuid to text + op.alter_column( + "users", + "id", + type_=sa.Text(), + existing_type=sa.UUID(), + postgresql_using="id::text", + ) + + # Step 3: Alter user_store_accounts.user_id from uuid to text + op.alter_column( + "user_store_accounts", + "user_id", + type_=sa.Text(), + existing_type=sa.UUID(), + postgresql_using="user_id::text", + ) + + # Step 4: Alter purchases.user_id from uuid to text + op.alter_column( + "purchases", + "user_id", + type_=sa.Text(), + existing_type=sa.UUID(), + postgresql_using="user_id::text", + ) + + # Step 5: Re-add FK constraints + op.execute( + text( + "ALTER TABLE user_store_accounts " + "ADD CONSTRAINT user_store_accounts_user_id_fkey " + "FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE" + ) + ) + op.execute( + text( + "ALTER TABLE purchases " + "ADD CONSTRAINT purchases_user_id_fkey " + "FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE" + ) + ) + + +def downgrade() -> None: + # Drop FK constraints + op.execute(text("ALTER TABLE user_store_accounts DROP CONSTRAINT IF EXISTS user_store_accounts_user_id_fkey")) + op.execute(text("ALTER TABLE purchases DROP CONSTRAINT IF EXISTS purchases_user_id_fkey")) + + # Revert users.id from text to uuid + op.alter_column( + "users", + "id", + type_=sa.UUID(), + existing_type=sa.Text(), + postgresql_using="id::uuid", + ) + + # Revert user_store_accounts.user_id from text to uuid + op.alter_column( + "user_store_accounts", + "user_id", + type_=sa.UUID(), + existing_type=sa.Text(), + postgresql_using="user_id::uuid", + ) + + # Revert purchases.user_id from text to uuid + op.alter_column( + "purchases", + "user_id", + type_=sa.UUID(), + existing_type=sa.Text(), + postgresql_using="user_id::uuid", + ) + + # Re-add FK constraints (PostgreSQL will auto-name them) + op.execute( + text( + "ALTER TABLE user_store_accounts " + "ADD CONSTRAINT user_store_accounts_user_id_fkey " + "FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE" + ) + ) + op.execute( + text( + "ALTER TABLE purchases " + "ADD CONSTRAINT purchases_user_id_fkey " + "FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE" + ) + ) diff --git a/src/cartsnitch_api/models/purchase.py b/src/cartsnitch_api/models/purchase.py index f57fde9..5a56cba 100644 --- a/src/cartsnitch_api/models/purchase.py +++ b/src/cartsnitch_api/models/purchase.py @@ -32,7 +32,7 @@ class Purchase(UUIDPrimaryKeyMixin, TimestampMixin, Base): __tablename__ = "purchases" - user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), nullable=False) + user_id: Mapped[str] = mapped_column(ForeignKey("users.id"), nullable=False) store_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("stores.id"), nullable=False) store_location_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("store_locations.id")) receipt_id: Mapped[str] = mapped_column(String(200), nullable=False) diff --git a/src/cartsnitch_api/models/user.py b/src/cartsnitch_api/models/user.py index 56482b0..2c87644 100644 --- a/src/cartsnitch_api/models/user.py +++ b/src/cartsnitch_api/models/user.py @@ -4,7 +4,7 @@ import uuid from datetime import datetime from typing import TYPE_CHECKING -from sqlalchemy import DateTime, ForeignKey, String, UniqueConstraint +from sqlalchemy import DateTime, ForeignKey, String, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship from cartsnitch_api.constants import AccountStatus @@ -16,11 +16,12 @@ if TYPE_CHECKING: from cartsnitch_api.models.store import Store -class User(UUIDPrimaryKeyMixin, TimestampMixin, Base): +class User(TimestampMixin, Base): """Application user.""" __tablename__ = "users" + id: Mapped[str] = mapped_column(Text, primary_key=True) email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True) hashed_password: Mapped[str] = mapped_column(String(255), nullable=False) display_name: Mapped[str | None] = mapped_column(String(100)) @@ -36,7 +37,7 @@ class UserStoreAccount(UUIDPrimaryKeyMixin, TimestampMixin, Base): __tablename__ = "user_store_accounts" __table_args__ = (UniqueConstraint("user_id", "store_id", name="uq_user_store_account"),) - user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), nullable=False) + user_id: Mapped[str] = mapped_column(ForeignKey("users.id"), nullable=False) store_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("stores.id"), nullable=False) session_data: Mapped[dict | None] = mapped_column(EncryptedJSON) session_expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) diff --git a/src/cartsnitch_api/schemas.py b/src/cartsnitch_api/schemas.py index 1ba727e..6547963 100644 --- a/src/cartsnitch_api/schemas.py +++ b/src/cartsnitch_api/schemas.py @@ -16,7 +16,7 @@ class UpdateUserRequest(BaseModel): class UserResponse(BaseModel): - id: UUID + id: str email: str display_name: str created_at: datetime From 1874de4beb4c8c30550882927f82d18ee47be33b Mon Sep 17 00:00:00 2001 From: "cartsnitch-engineer[bot]" <269717931+cartsnitch-engineer[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 21:55:00 +0000 Subject: [PATCH 4/4] fix(api): run Alembic migrations on startup (#90) Merged by Coupon Carl (CEO). QA approved, CTO approved. CI green (lighthouse failure is known/tracked). cc @cpfarhood --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e271e94..7c3df44 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,4 +30,4 @@ EXPOSE 8000 HEALTHCHECK --interval=30s --timeout=3s \ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" -CMD ["uvicorn", "cartsnitch_api.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file +CMD ["sh", "-c", "python -m alembic upgrade head && uvicorn cartsnitch_api.main:app --host 0.0.0.0 --port 8000"] \ No newline at end of file