Compare commits

..

5 Commits

Author SHA1 Message Date
Chris Farhood 33b920bf2c Add .mcp.json
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / build-and-push (push) Has been cancelled
CI / grype (push) Has been cancelled
CI / deploy-dev (push) Has been cancelled
CI / deploy-uat (push) Has been cancelled
2026-05-25 21:46:45 +00:00
savannah-savings-cto[bot] 8128b3a76f 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.
2026-04-19 14:02:18 +00:00
cartsnitch-ceo[bot] f159d50f7c Merge pull request #4 from cartsnitch/dev
chore: promote dev to uat — receiptwitness migration CI fixes
2026-04-19 13:25:40 +00:00
cartsnitch-ceo[bot] c4880d3553 Merge pull request #3 from cartsnitch/betty/car-724-ci-fix
fix: resolve CI failures — SQLite incompatibility and ruff lint errors
2026-04-19 13:25:16 +00:00
Barcode Betty 873f53b9fc fix: resolve CI failures — SQLite incompatibility and ruff lint errors
- Remove PostgreSQL-specific server_default from User.email_inbound_token.
  The column has a Python-side default (secrets.token_urlsafe) that works
  for both SQLite and PostgreSQL. The gen_random_bytes() server_default
  caused sqlite table creation to fail.

- Add missing back_populates relationships to stub models so SQLAlchemy
  mapper configuration succeeds. Purchase.user and Store.user_accounts
  were missing, causing "has no property" errors during Base.metadata.create_all.

- Auto-fix ruff import sorting (I001) across all source and test files.

- Manually fix line-too-long (E501) in config.py.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-19 13:06:47 +00:00
15 changed files with 60 additions and 27 deletions
+11
View File
@@ -0,0 +1,11 @@
{
"mcpServers": {
"gitea": {
"type": "http",
"url": "https://git-mcp.farh.net/mcp",
"headers": {
"Authorization": "Bearer ${GITEA_TOKEN}"
}
}
}
}
@@ -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: