Merge commit '4cf6f91e954b770198578bcb8db5d98ac964bfed' as 'common'

This commit is contained in:
Coupon Carl
2026-03-28 02:24:14 +00:00
66 changed files with 7044 additions and 0 deletions
@@ -0,0 +1,49 @@
"""Pydantic v2 schemas for inter-service API contracts."""
from cartsnitch_common.schemas.coupon import CouponCreate, CouponRead
from cartsnitch_common.schemas.events import EventEnvelope
from cartsnitch_common.schemas.price import PriceHistoryCreate, PriceHistoryRead
from cartsnitch_common.schemas.product import NormalizedProductCreate, NormalizedProductRead
from cartsnitch_common.schemas.purchase import (
PurchaseCreate,
PurchaseItemCreate,
PurchaseItemRead,
PurchaseRead,
)
from cartsnitch_common.schemas.shrinkflation import ShrinkflationEventCreate, ShrinkflationEventRead
from cartsnitch_common.schemas.store import (
StoreCreate,
StoreLocationCreate,
StoreLocationRead,
StoreRead,
)
from cartsnitch_common.schemas.user import (
UserCreate,
UserRead,
UserStoreAccountCreate,
UserStoreAccountRead,
)
__all__ = [
"StoreCreate",
"StoreRead",
"StoreLocationCreate",
"StoreLocationRead",
"UserCreate",
"UserRead",
"UserStoreAccountCreate",
"UserStoreAccountRead",
"PurchaseCreate",
"PurchaseRead",
"PurchaseItemCreate",
"PurchaseItemRead",
"NormalizedProductCreate",
"NormalizedProductRead",
"PriceHistoryCreate",
"PriceHistoryRead",
"CouponCreate",
"CouponRead",
"ShrinkflationEventCreate",
"ShrinkflationEventRead",
"EventEnvelope",
]
@@ -0,0 +1,45 @@
"""Coupon Pydantic schemas."""
import uuid
from datetime import date, datetime
from decimal import Decimal
from pydantic import BaseModel
from cartsnitch_common.constants import DiscountType
class CouponCreate(BaseModel):
store_id: uuid.UUID
normalized_product_id: uuid.UUID | None = None
title: str
description: str | None = None
discount_type: DiscountType
discount_value: Decimal | None = None
min_purchase: Decimal | None = None
valid_from: date | None = None
valid_to: date | None = None
requires_clip: bool = False
coupon_code: str | None = None
source_url: str | None = None
class CouponRead(BaseModel):
model_config = {"from_attributes": True}
id: uuid.UUID
store_id: uuid.UUID
normalized_product_id: uuid.UUID | None
title: str
description: str | None
discount_type: DiscountType
discount_value: Decimal | None
min_purchase: Decimal | None
valid_from: date | None
valid_to: date | None
requires_clip: bool
coupon_code: str | None
source_url: str | None
scraped_at: datetime | None
created_at: datetime
updated_at: datetime
@@ -0,0 +1,17 @@
"""Redis pub/sub event envelope and payload schemas."""
from datetime import datetime
from typing import Any
from pydantic import BaseModel
from cartsnitch_common.constants import EventType
class EventEnvelope(BaseModel):
"""Standard event wrapper for all Redis pub/sub messages."""
event_type: EventType
timestamp: datetime
service: str
payload: dict[str, Any]
@@ -0,0 +1,38 @@
"""PriceHistory Pydantic schemas."""
import uuid
from datetime import date, datetime
from decimal import Decimal
from pydantic import BaseModel
from cartsnitch_common.constants import PriceSource
class PriceHistoryCreate(BaseModel):
normalized_product_id: uuid.UUID
store_id: uuid.UUID
observed_date: date
regular_price: Decimal
sale_price: Decimal | None = None
loyalty_price: Decimal | None = None
coupon_price: Decimal | None = None
source: PriceSource
purchase_item_id: uuid.UUID | None = None
class PriceHistoryRead(BaseModel):
model_config = {"from_attributes": True}
id: uuid.UUID
normalized_product_id: uuid.UUID
store_id: uuid.UUID
observed_date: date
regular_price: Decimal
sale_price: Decimal | None
loyalty_price: Decimal | None
coupon_price: Decimal | None
source: PriceSource
purchase_item_id: uuid.UUID | None
created_at: datetime
updated_at: datetime
@@ -0,0 +1,33 @@
"""NormalizedProduct Pydantic schemas."""
import uuid
from datetime import datetime
from pydantic import BaseModel
from cartsnitch_common.constants import ProductCategory, SizeUnit
class NormalizedProductCreate(BaseModel):
canonical_name: str
category: ProductCategory | None = None
subcategory: str | None = None
brand: str | None = None
size: str | None = None
size_unit: SizeUnit | None = None
upc_variants: list[str] = []
class NormalizedProductRead(BaseModel):
model_config = {"from_attributes": True}
id: uuid.UUID
canonical_name: str
category: ProductCategory | None
subcategory: str | None
brand: str | None
size: str | None
size_unit: SizeUnit | None
upc_variants: list | None
created_at: datetime
updated_at: datetime
@@ -0,0 +1,73 @@
"""Purchase and PurchaseItem Pydantic schemas."""
import uuid
from datetime import date, datetime
from decimal import Decimal
from pydantic import BaseModel
class PurchaseItemCreate(BaseModel):
product_name_raw: str
upc: str | None = None
quantity: Decimal = Decimal("1")
unit_price: Decimal
extended_price: Decimal
regular_price: Decimal | None = None
sale_price: Decimal | None = None
coupon_discount: Decimal | None = None
loyalty_discount: Decimal | None = None
category_raw: str | None = None
normalized_product_id: uuid.UUID | None = None
class PurchaseItemRead(BaseModel):
model_config = {"from_attributes": True}
id: uuid.UUID
purchase_id: uuid.UUID
product_name_raw: str
upc: str | None
quantity: Decimal
unit_price: Decimal
extended_price: Decimal
regular_price: Decimal | None
sale_price: Decimal | None
coupon_discount: Decimal | None
loyalty_discount: Decimal | None
category_raw: str | None
normalized_product_id: uuid.UUID | None
class PurchaseCreate(BaseModel):
user_id: uuid.UUID
store_id: uuid.UUID
store_location_id: uuid.UUID | None = None
receipt_id: str
purchase_date: date
total: Decimal
subtotal: Decimal | None = None
tax: Decimal | None = None
savings_total: Decimal | None = None
source_url: str | None = None
raw_data: dict | None = None
items: list[PurchaseItemCreate] = []
class PurchaseRead(BaseModel):
model_config = {"from_attributes": True}
id: uuid.UUID
user_id: uuid.UUID
store_id: uuid.UUID
store_location_id: uuid.UUID | None
receipt_id: str
purchase_date: date
total: Decimal
subtotal: Decimal | None
tax: Decimal | None
savings_total: Decimal | None
source_url: str | None
ingested_at: datetime
created_at: datetime
updated_at: datetime
@@ -0,0 +1,40 @@
"""ShrinkflationEvent Pydantic schemas."""
import uuid
from datetime import date, datetime
from decimal import Decimal
from pydantic import BaseModel
from cartsnitch_common.constants import SizeUnit
class ShrinkflationEventCreate(BaseModel):
normalized_product_id: uuid.UUID
detected_date: date
old_size: str
new_size: str
old_unit: SizeUnit
new_unit: SizeUnit
price_at_old_size: Decimal | None = None
price_at_new_size: Decimal | None = None
confidence: Decimal = Decimal("1.00")
notes: str | None = None
class ShrinkflationEventRead(BaseModel):
model_config = {"from_attributes": True}
id: uuid.UUID
normalized_product_id: uuid.UUID
detected_date: date
old_size: str
new_size: str
old_unit: SizeUnit
new_unit: SizeUnit
price_at_old_size: Decimal | None
price_at_new_size: Decimal | None
confidence: Decimal
notes: str | None
created_at: datetime
updated_at: datetime
@@ -0,0 +1,52 @@
"""Store and StoreLocation Pydantic schemas."""
import uuid
from datetime import datetime
from pydantic import BaseModel
from cartsnitch_common.constants import StoreSlug
class StoreCreate(BaseModel):
name: str
slug: StoreSlug
logo_url: str | None = None
website_url: str | None = None
class StoreRead(BaseModel):
model_config = {"from_attributes": True}
id: uuid.UUID
name: str
slug: StoreSlug
logo_url: str | None
website_url: str | None
created_at: datetime
updated_at: datetime
class StoreLocationCreate(BaseModel):
store_id: uuid.UUID
address: str
city: str
state: str
zip: str
lat: float | None = None
lng: float | None = None
class StoreLocationRead(BaseModel):
model_config = {"from_attributes": True}
id: uuid.UUID
store_id: uuid.UUID
address: str
city: str
state: str
zip: str
lat: float | None
lng: float | None
created_at: datetime
updated_at: datetime
@@ -0,0 +1,44 @@
"""User and UserStoreAccount Pydantic schemas."""
import uuid
from datetime import datetime
from pydantic import BaseModel, EmailStr
from cartsnitch_common.constants import AccountStatus
class UserCreate(BaseModel):
email: EmailStr
password: str
display_name: str | None = None
class UserRead(BaseModel):
model_config = {"from_attributes": True}
id: uuid.UUID
email: str
display_name: str | None
created_at: datetime
updated_at: datetime
class UserStoreAccountCreate(BaseModel):
user_id: uuid.UUID
store_id: uuid.UUID
session_data: dict | None = None
status: AccountStatus = AccountStatus.ACTIVE
class UserStoreAccountRead(BaseModel):
model_config = {"from_attributes": True}
id: uuid.UUID
user_id: uuid.UUID
store_id: uuid.UUID
status: AccountStatus
session_expires_at: datetime | None
last_sync_at: datetime | None
created_at: datetime
updated_at: datetime