feat: merge cartsnitch/api into api/ subdirectory

Consolidate API gateway service into monorepo.
Squashed from https://github.com/cartsnitch/api main (89bacb1).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Coupon Carl
2026-03-28 02:24:02 +00:00
commit b7e6f637a7
91 changed files with 6296 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
"""Alert service — price and shrinkflation alerts for users.
Alerts are generated by StickerShock and ShrinkRay services and written to the DB.
This service reads them for the API gateway.
"""
from uuid import UUID
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
class AlertService:
def __init__(self, db: AsyncSession) -> None:
self.db = db
async def list_alerts(self, user_id: UUID) -> list[dict]:
"""List shrinkflation events for products the user has purchased."""
from cartsnitch_api.models import Purchase, PurchaseItem, ShrinkflationEvent
# Get product IDs from user's purchases
items_result = await self.db.execute(
select(PurchaseItem.normalized_product_id)
.join(Purchase)
.where(
Purchase.user_id == user_id,
PurchaseItem.normalized_product_id.isnot(None),
)
.distinct()
)
product_ids = [row[0] for row in items_result.all()]
if not product_ids:
return []
result = await self.db.execute(
select(ShrinkflationEvent)
.where(ShrinkflationEvent.normalized_product_id.in_(product_ids))
.options(selectinload(ShrinkflationEvent.normalized_product))
.order_by(ShrinkflationEvent.detected_date.desc())
)
events = result.scalars().all()
return [
{
"id": e.id,
"alert_type": "shrinkflation",
"product_id": e.normalized_product_id,
"product_name": e.normalized_product.canonical_name,
"message": (
f"Size changed from {e.old_size}{e.old_unit} to {e.new_size}{e.new_unit}"
),
"triggered_at": e.detected_date,
"read": False,
}
for e in events
]
async def get_settings(self, user_id: UUID) -> dict:
# Alert settings would be stored in a user_settings table.
# For now, return defaults since the table doesn't exist yet in common lib.
return {
"price_increase_threshold_pct": 5.0,
"shrinkflation_enabled": True,
"email_notifications": False,
}
async def update_settings(self, user_id: UUID, **fields) -> dict:
# Would update user_settings table. Return merged defaults for now.
current = await self.get_settings(user_id)
for k, v in fields.items():
if v is not None and k in current:
current[k] = v
return current