chore: promote uat to main — receiptwitness migration CI fixes

Merged to production. UAT regression and security review both passed.

- UAT: PASS (Deal Dottie — CAR-733)
- Security: PASS (Stockboy Steve)
- Code CI (lint + test): PASS on uat commit f159d50f

Note: build-and-push has a GHCR permission_denied failure (write_package) — separate infra issue, does not affect code correctness.
This commit was merged in pull request #5.
This commit is contained in:
savannah-savings-cto[bot]
2026-04-19 14:02:18 +00:00
committed by GitHub
14 changed files with 49 additions and 27 deletions
@@ -34,4 +34,4 @@ def upgrade() -> None:
def downgrade() -> None:
op.drop_constraint("uq_users_email_inbound_token", "users", type_="unique")
op.drop_column("users", "email_inbound_token")
op.drop_column("users", "email_inbound_token")
+2 -2
View File
@@ -3,7 +3,6 @@
from pydantic import model_validator
from pydantic_settings import BaseSettings
_PLACEHOLDER_VALUES = {"change-me-in-production"}
@@ -40,7 +39,8 @@ class ReceiptWitnessSettings(BaseSettings):
if not self.session_encryption_key or self.session_encryption_key in _PLACEHOLDER_VALUES:
errors.append(
"RW_SESSION_ENCRYPTION_KEY must be set to a secure value. "
'Generate one with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"'
"Generate one with: python -c \"from cryptography.fernet import Fernet; "
'print(Fernet.generate_key().decode())"'
)
if self.notifications_enabled and not self.resend_api_key:
errors.append(
+2 -2
View File
@@ -7,12 +7,12 @@ from datetime import UTC, datetime
from decimal import Decimal
import redis.asyncio as aioredis
from receiptwitness.shared.database import get_async_session_factory
from receiptwitness.shared.models import User
from sqlalchemy import select
from receiptwitness.config import settings
from receiptwitness.notifications.email import send_receipt_notification
from receiptwitness.shared.database import get_async_session_factory
from receiptwitness.shared.models import User
logger = logging.getLogger(__name__)
+3 -3
View File
@@ -7,9 +7,6 @@ and batch matching for purchase ingestion.
import uuid
from dataclasses import dataclass
from receiptwitness.shared.constants import MatchConfidence
from receiptwitness.shared.models import NormalizedProduct
from receiptwitness.shared.schemas import PurchaseItemCreate
from sqlalchemy.orm import Session
from receiptwitness.pipeline.normalization import (
@@ -18,6 +15,9 @@ from receiptwitness.pipeline.normalization import (
extract_size_info,
normalize_product,
)
from receiptwitness.shared.constants import MatchConfidence
from receiptwitness.shared.models import NormalizedProduct
from receiptwitness.shared.schemas import PurchaseItemCreate
# Re-export for convenience
ConfidenceLevel = MatchConfidence
+3 -2
View File
@@ -10,11 +10,12 @@ import re
from dataclasses import dataclass
from enum import StrEnum
from receiptwitness.shared.models import NormalizedProduct
from sqlalchemy import cast, func, select, String
from sqlalchemy import String, cast, func, select
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Session
from receiptwitness.shared.models import NormalizedProduct
class MatchMethod(StrEnum):
"""How a product match was determined."""
+2 -2
View File
@@ -2,12 +2,12 @@
from receiptwitness.shared.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
from receiptwitness.shared.models.product import NormalizedProduct
from receiptwitness.shared.models.user import User, UserStoreAccount
from receiptwitness.shared.models.stub_purchase import Purchase, PurchaseItem
# Stub models — needed for relationship resolution but not directly used by receiptwitness.
# Full definitions live in cartsnitch/common.
from receiptwitness.shared.models.stub_store import Store, StoreLocation
from receiptwitness.shared.models.stub_purchase import Purchase, PurchaseItem
from receiptwitness.shared.models.user import User, UserStoreAccount
__all__ = [
"Base",
@@ -8,12 +8,26 @@ UserStoreAccount. The canonical definitions live in cartsnitch/common.
import uuid
from datetime import date, datetime
from decimal import Decimal
from typing import TYPE_CHECKING
from sqlalchemy import JSON, Date, DateTime, ForeignKey, Index, Numeric, String, UniqueConstraint, func
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import (
JSON,
Date,
DateTime,
ForeignKey,
Index,
Numeric,
String,
UniqueConstraint,
func,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from receiptwitness.shared.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
if TYPE_CHECKING:
from receiptwitness.shared.models.user import User
class Purchase(UUIDPrimaryKeyMixin, TimestampMixin, Base):
"""Stub: a shopping trip/receipt. Full definition in cartsnitch/common."""
@@ -37,6 +51,9 @@ class Purchase(UUIDPrimaryKeyMixin, TimestampMixin, Base):
nullable=False,
)
# Relationships (stubs — canonical definitions in cartsnitch/common)
user: Mapped["User"] = relationship(back_populates="purchases")
__table_args__ = (
Index("ix_purchases_user_store", "user_id", "store_id"),
UniqueConstraint("user_id", "store_id", "receipt_id", name="uq_purchase_receipt"),
@@ -6,6 +6,7 @@ UserStoreAccount. The canonical definitions live in cartsnitch/common.
"""
import uuid
from typing import TYPE_CHECKING
from sqlalchemy import Float, ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
@@ -13,6 +14,9 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from receiptwitness.shared.constants import StoreSlug
from receiptwitness.shared.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
if TYPE_CHECKING:
from receiptwitness.shared.models.user import UserStoreAccount
class Store(UUIDPrimaryKeyMixin, TimestampMixin, Base):
"""Stub: canonical retailer. Full definition in cartsnitch/common."""
@@ -24,6 +28,9 @@ class Store(UUIDPrimaryKeyMixin, TimestampMixin, Base):
logo_url: Mapped[str | None] = mapped_column(String(500))
website_url: Mapped[str | None] = mapped_column(String(500))
# Relationships (stubs — canonical definitions in cartsnitch/common)
user_accounts: Mapped[list["UserStoreAccount"]] = relationship(back_populates="store")
class StoreLocation(UUIDPrimaryKeyMixin, TimestampMixin, Base):
"""Stub: physical store location. Full definition in cartsnitch/common."""
+1 -4
View File
@@ -5,7 +5,7 @@ import uuid
from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import JSON, Boolean, DateTime, ForeignKey, String, Text, UniqueConstraint, text
from sqlalchemy import JSON, Boolean, DateTime, ForeignKey, String, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from receiptwitness.shared.constants import AccountStatus
@@ -27,9 +27,6 @@ class User(UUIDPrimaryKeyMixin, TimestampMixin, Base):
nullable=False,
unique=True,
default=lambda: secrets.token_urlsafe(16),
server_default=text(
"replace(replace(trim(trailing '=' from encode(gen_random_bytes(16), 'base64')), '+', '-'), '/', '_')"
),
)
hashed_password: Mapped[str | None] = mapped_column(String(255), nullable=True)
display_name: Mapped[str | None] = mapped_column(String(100))
+2 -2
View File
@@ -3,8 +3,6 @@
import asyncio
import logging
from receiptwitness.shared.database import get_async_session_factory
from receiptwitness.shared.models import User
from sqlalchemy import select
from receiptwitness.config import settings
@@ -15,6 +13,8 @@ from receiptwitness.parsers.email.kroger import KrogerEmailParser
from receiptwitness.parsers.email.meijer import MeijerEmailParser
from receiptwitness.parsers.email.target import TargetEmailParser
from receiptwitness.queue.email import ack_email, consume_emails, get_redis
from receiptwitness.shared.database import get_async_session_factory
from receiptwitness.shared.models import User
logger = logging.getLogger(__name__)
+1
View File
@@ -1,4 +1,5 @@
import pytest
from receiptwitness.config import ReceiptWitnessSettings
+2 -1
View File
@@ -1,10 +1,11 @@
"""Shared test fixtures for pipeline tests."""
import pytest
from receiptwitness.shared.models import Base
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from receiptwitness.shared.models import Base
@pytest.fixture
def engine():
+3 -4
View File
@@ -4,16 +4,15 @@ import uuid
from datetime import UTC, datetime
from decimal import Decimal
from receiptwitness.shared.constants import MatchConfidence
from receiptwitness.shared.models import NormalizedProduct
from receiptwitness.shared.schemas import PurchaseItemCreate
from receiptwitness.pipeline.matching import (
ProductMatcher,
classify_confidence,
match_purchase_item,
)
from receiptwitness.pipeline.normalization import MatchMethod
from receiptwitness.shared.constants import MatchConfidence
from receiptwitness.shared.models import NormalizedProduct
from receiptwitness.shared.schemas import PurchaseItemCreate
class TestClassifyConfidence:
+1 -2
View File
@@ -3,8 +3,6 @@
import uuid
from datetime import UTC, datetime
from receiptwitness.shared.models import NormalizedProduct
from receiptwitness.pipeline.normalization import (
MatchMethod,
clean_name,
@@ -14,6 +12,7 @@ from receiptwitness.pipeline.normalization import (
match_by_upc,
normalize_product,
)
from receiptwitness.shared.models import NormalizedProduct
class TestCleanName: