BuyingAgent/backend/app/persistence/models.py

250 lines
12 KiB
Python
Raw Permalink Normal View History

from datetime import datetime
from decimal import Decimal
from typing import Any
from uuid import UUID
from sqlalchemy import (
CheckConstraint,
DateTime,
ForeignKey,
ForeignKeyConstraint,
Numeric,
String,
Text,
UniqueConstraint,
)
from sqlalchemy.dialects.postgresql import JSONB, UUID as PGUUID
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class AttributeDefinitionRecord(Base):
__tablename__ = "attribute_definitions"
__table_args__ = (
CheckConstraint(
"value_type IN ('number', 'string', 'boolean', 'string_list')",
name="ck_attribute_definitions_value_type",
),
)
key: Mapped[str] = mapped_column(String(128), primary_key=True)
value_type: Mapped[str] = mapped_column(String(32), nullable=False)
canonical_unit: Mapped[str | None] = mapped_column(String(32))
minimum: Mapped[Decimal | None] = mapped_column(Numeric(12, 4))
maximum: Mapped[Decimal | None] = mapped_column(Numeric(12, 4))
description: Mapped[str | None] = mapped_column(Text)
class SourceRecord(Base):
__tablename__ = "sources"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
adapter_type: Mapped[str] = mapped_column(String(64), nullable=False)
class ScoringModelRecord(Base):
__tablename__ = "scoring_models"
version: Mapped[str] = mapped_column(String(64), primary_key=True)
algorithm_id: Mapped[str] = mapped_column(String(128), nullable=False)
definition: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
definition_hash: Mapped[str] = mapped_column(String(64), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
class ProductProfileRecord(Base):
__tablename__ = "product_profiles"
__table_args__ = (
CheckConstraint("version > 0", name="ck_product_profiles_version"),
)
id: Mapped[str] = mapped_column(String(128), primary_key=True)
version: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(256), nullable=False)
category: Mapped[str] = mapped_column(String(128), nullable=False)
scoring_version: Mapped[str] = mapped_column(ForeignKey("scoring_models.version"), nullable=False)
definition: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
definition_hash: Mapped[str] = mapped_column(String(64), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
class ProductRecord(Base):
__tablename__ = "products"
id: Mapped[UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True)
category: Mapped[str] = mapped_column(String(128), nullable=False)
brand: Mapped[str] = mapped_column(String(128), nullable=False)
name: Mapped[str] = mapped_column(String(256), nullable=False)
model: Mapped[str | None] = mapped_column(String(128))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
variants: Mapped[list["ProductVariantRecord"]] = relationship(back_populates="product")
class ProductVariantRecord(Base):
__tablename__ = "product_variants"
id: Mapped[UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True)
product_id: Mapped[UUID] = mapped_column(ForeignKey("products.id"), nullable=False, index=True)
name: Mapped[str] = mapped_column(String(256), nullable=False)
manufacturer_sku: Mapped[str | None] = mapped_column(String(128))
identifiers: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
product: Mapped[ProductRecord] = relationship(back_populates="variants")
class VariantAttributeRecord(Base):
__tablename__ = "variant_attributes"
__table_args__ = (
CheckConstraint("confidence >= 0 AND confidence <= 1", name="ck_variant_attributes_confidence"),
CheckConstraint(
"normalization_status IN ('normalized', 'unknown')",
name="ck_variant_attributes_status",
),
)
id: Mapped[UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True)
variant_id: Mapped[UUID] = mapped_column(ForeignKey("product_variants.id"), nullable=False)
attribute_key: Mapped[str] = mapped_column(ForeignKey("attribute_definitions.key"), nullable=False)
source_id: Mapped[str] = mapped_column(ForeignKey("sources.id"), nullable=False)
raw_value: Mapped[Any] = mapped_column(JSONB, nullable=False)
normalized_value: Mapped[Any] = mapped_column(JSONB)
unit: Mapped[str | None] = mapped_column(String(32))
normalization_status: Mapped[str] = mapped_column(String(32), nullable=False)
normalizer_version: Mapped[str] = mapped_column(String(64), nullable=False)
confidence: Mapped[Decimal] = mapped_column(Numeric(4, 3), nullable=False)
source_reference: Mapped[str] = mapped_column(String(256), nullable=False)
observed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
ingested_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
normalization_error: Mapped[str | None] = mapped_column(Text)
class OfferRecord(Base):
__tablename__ = "offers"
__table_args__ = (
UniqueConstraint("source_id", "external_id", name="uq_offers_source_external"),
CheckConstraint("currency ~ '^[A-Z]{3}$'", name="ck_offers_currency"),
CheckConstraint("condition IN ('new', 'refurbished', 'used')", name="ck_offers_condition"),
)
id: Mapped[UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True)
variant_id: Mapped[UUID] = mapped_column(ForeignKey("product_variants.id"), nullable=False, index=True)
source_id: Mapped[str] = mapped_column(ForeignKey("sources.id"), nullable=False)
external_id: Mapped[str] = mapped_column(String(256), nullable=False)
seller: Mapped[str] = mapped_column(String(256), nullable=False)
url: Mapped[str | None] = mapped_column(Text)
market: Mapped[str] = mapped_column(String(16), nullable=False)
currency: Mapped[str] = mapped_column(String(3), nullable=False)
condition: Mapped[str] = mapped_column(String(32), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
class PriceObservationRecord(Base):
__tablename__ = "price_observations"
__table_args__ = (
UniqueConstraint("offer_id", "external_observation_id", name="uq_price_obs_offer_external"),
CheckConstraint("item_price > 0", name="ck_price_obs_item_price"),
CheckConstraint("shipping_price IS NULL OR shipping_price >= 0", name="ck_price_obs_shipping"),
CheckConstraint("confidence >= 0 AND confidence <= 1", name="ck_price_obs_confidence"),
CheckConstraint(
"availability IN ('in_stock', 'out_of_stock', 'unknown')",
name="ck_price_obs_availability",
),
CheckConstraint("currency ~ '^[A-Z]{3}$'", name="ck_price_obs_currency"),
)
id: Mapped[UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True)
offer_id: Mapped[UUID] = mapped_column(ForeignKey("offers.id"), nullable=False)
source_id: Mapped[str] = mapped_column(ForeignKey("sources.id"), nullable=False)
external_observation_id: Mapped[str] = mapped_column(String(256), nullable=False)
observed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
ingested_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
item_price: Mapped[Decimal] = mapped_column(Numeric(12, 2), nullable=False)
shipping_price: Mapped[Decimal | None] = mapped_column(Numeric(12, 2))
currency: Mapped[str] = mapped_column(String(3), nullable=False)
availability: Mapped[str] = mapped_column(String(32), nullable=False)
raw_payload: Mapped[Any] = mapped_column(JSONB, nullable=False)
confidence: Mapped[Decimal] = mapped_column(Numeric(4, 3), nullable=False)
class EvaluationRecord(Base):
__tablename__ = "evaluations"
__table_args__ = (
ForeignKeyConstraint(
["profile_id", "profile_version"],
["product_profiles.id", "product_profiles.version"],
name="fk_evaluations_profile",
),
CheckConstraint(
"product_eligibility IN ('ELIGIBLE', 'INELIGIBLE', 'UNKNOWN')",
name="ck_eval_product_eligibility",
),
CheckConstraint(
"offer_eligibility IN ('ELIGIBLE', 'INELIGIBLE', 'UNKNOWN')",
name="ck_eval_offer_eligibility",
),
CheckConstraint(
"overall_eligibility IN ('ELIGIBLE', 'INELIGIBLE', 'UNKNOWN')",
name="ck_eval_overall_eligibility",
),
CheckConstraint(
"product_score IS NULL OR (product_score >= 0 AND product_score <= 100)",
name="ck_eval_product_score",
),
CheckConstraint(
"price_score IS NULL OR (price_score >= 0 AND price_score <= 100)",
name="ck_eval_price_score",
),
CheckConstraint(
"deal_score IS NULL OR (deal_score >= 0 AND deal_score <= 100)",
name="ck_eval_deal_score",
),
)
id: Mapped[UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True)
variant_id: Mapped[UUID] = mapped_column(ForeignKey("product_variants.id"), nullable=False, index=True)
offer_id: Mapped[UUID] = mapped_column(ForeignKey("offers.id"), nullable=False)
profile_id: Mapped[str] = mapped_column(String(128), nullable=False)
profile_version: Mapped[int] = mapped_column(nullable=False)
scoring_version: Mapped[str] = mapped_column(ForeignKey("scoring_models.version"), nullable=False)
as_of: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
snapshot_cutoff: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
scoring_hash: Mapped[str] = mapped_column(String(64), nullable=False)
engine_build: Mapped[str] = mapped_column(String(64), nullable=False)
product_eligibility: Mapped[str] = mapped_column(String(16), nullable=False)
offer_eligibility: Mapped[str] = mapped_column(String(16), nullable=False)
overall_eligibility: Mapped[str] = mapped_column(String(16), nullable=False)
product_score: Mapped[Decimal | None] = mapped_column(Numeric(6, 2))
price_score: Mapped[Decimal | None] = mapped_column(Numeric(6, 2))
deal_score: Mapped[Decimal | None] = mapped_column(Numeric(6, 2))
classification: Mapped[str] = mapped_column(String(32), nullable=False)
price_reference_scope: Mapped[str] = mapped_column(String(32), nullable=False)
input_snapshot: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
result_snapshot: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
components: Mapped[list["EvaluationComponentRecord"]] = relationship(back_populates="evaluation")
class EvaluationComponentRecord(Base):
__tablename__ = "evaluation_components"
__table_args__ = (
UniqueConstraint("evaluation_id", "kind", "component_key", name="uq_eval_component"),
)
id: Mapped[UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True)
evaluation_id: Mapped[UUID] = mapped_column(ForeignKey("evaluations.id"), nullable=False)
kind: Mapped[str] = mapped_column(String(64), nullable=False)
component_key: Mapped[str] = mapped_column(String(128), nullable=False)
raw_score: Mapped[Decimal | None] = mapped_column(Numeric(12, 6))
weight: Mapped[Decimal | None] = mapped_column(Numeric(12, 6))
contribution: Mapped[Decimal | None] = mapped_column(Numeric(12, 6))
details: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
evaluation: Mapped[EvaluationRecord] = relationship(back_populates="components")