BuyingAgent/backend/app/persistence/discovery_models.py
Lars 68a8e3d945
Some checks failed
Deploy Development / deploy (push) Successful in 2m6s
Test Suite / pytest-backend (push) Failing after 14s
Test Suite / smoke-dev (push) Successful in 1s
Test Suite / frontend-checks (push) Successful in 28s
Uebernimm Enum-Listen und Filtergruppen in die Taxonomie, speichere Suchtreffer als Kandidaten und lasse CI das vorhandene API-Image nutzen.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-04 15:12:51 +02:00

325 lines
18 KiB
Python

from datetime import datetime
from decimal import Decimal
from typing import Any
from uuid import UUID
from sqlalchemy import Boolean, DateTime, ForeignKey, ForeignKeyConstraint, Integer, Numeric, String, Text, UniqueConstraint
from sqlalchemy.dialects.postgresql import JSONB, UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column
from app.persistence.models import Base
class ProductCategoryRecord(Base):
__tablename__ = "product_categories"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
name: Mapped[str] = mapped_column(String(256), nullable=False)
status: Mapped[str] = mapped_column(String(32), nullable=False, default="ACTIVE")
class BuyingProjectRecord(Base):
__tablename__ = "buying_projects"
__table_args__ = (
ForeignKeyConstraint(
["template_id", "template_version"],
["product_profiles.id", "product_profiles.version"],
name="fk_buying_projects_template",
),
)
id: Mapped[UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True)
name: Mapped[str] = mapped_column(String(256), nullable=False)
status: Mapped[str] = mapped_column(String(32), nullable=False)
workspace_id: Mapped[str | None] = mapped_column(String(64))
product_category_id: Mapped[str] = mapped_column(ForeignKey("product_categories.id"), nullable=False)
market: Mapped[str] = mapped_column(String(8), nullable=False)
currency: Mapped[str] = mapped_column(String(3), nullable=False)
target_budget: Mapped[Decimal | None] = mapped_column(Numeric(12, 2))
template_id: Mapped[str | None] = mapped_column(String(128))
template_version: Mapped[int | None] = mapped_column(Integer)
shortlist_size: Mapped[int] = mapped_column(Integer, nullable=False, default=5)
criteria_version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
archived_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
@property
def profile_id(self) -> str | None:
return self.template_id
@property
def profile_version(self) -> int | None:
return self.template_version
class BuyingProjectCriteriaRecord(Base):
__tablename__ = "buying_project_criteria"
project_id: Mapped[UUID] = mapped_column(ForeignKey("buying_projects.id"), primary_key=True)
version: Mapped[int] = mapped_column(Integer, primary_key=True)
definition_hash: Mapped[str] = mapped_column(String(64), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
class ProjectCriterionRecord(Base):
__tablename__ = "project_criteria"
__table_args__ = (
ForeignKeyConstraint(
["project_id", "version"],
["buying_project_criteria.project_id", "buying_project_criteria.version"],
name="fk_project_criteria_version",
),
UniqueConstraint("project_id", "version", "item_id", name="uq_project_criterion_item"),
)
id: Mapped[UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True)
project_id: Mapped[UUID] = mapped_column(nullable=False)
version: Mapped[int] = mapped_column(Integer, nullable=False)
item_id: Mapped[str] = mapped_column(String(128), nullable=False)
criterion_definition_id: Mapped[UUID] = mapped_column(ForeignKey("criterion_definitions.id"), nullable=False)
criterion_key: Mapped[str | None] = mapped_column(String(128))
operator: Mapped[str] = mapped_column(String(16), nullable=False)
value: Mapped[Any] = mapped_column(JSONB)
enabled_for_discovery: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
@property
def mandatory(self) -> bool:
return self.enabled_for_discovery
@property
def criteria_version(self) -> int:
return self.version
class SourceCategoryRecord(Base):
__tablename__ = "source_categories"
__table_args__ = (UniqueConstraint("source_id", "external_category_key", name="uq_source_category"),)
id: Mapped[UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True)
source_id: Mapped[str] = mapped_column(ForeignKey("sources.id"), nullable=False)
external_category_key: Mapped[str] = mapped_column(String(64), nullable=False)
name: Mapped[str] = mapped_column(String(256), nullable=False)
source_url: Mapped[str] = mapped_column(Text, nullable=False)
product_category_id: Mapped[str | None] = mapped_column(ForeignKey("product_categories.id"))
canonical_category: Mapped[str | None] = mapped_column(String(128))
status: Mapped[str] = mapped_column(String(32), nullable=False)
taxonomy_status: Mapped[str] = mapped_column(String(32), nullable=False, default="NOT_LEARNED")
last_taxonomy_scan_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
last_verified_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
class SourceFilterDefinitionRecord(Base):
__tablename__ = "source_filter_definitions"
__table_args__ = (UniqueConstraint("source_category_id", "external_filter_key", name="uq_source_filter"),)
id: Mapped[UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True)
source_category_id: Mapped[UUID] = mapped_column(ForeignKey("source_categories.id"), nullable=False)
external_filter_key: Mapped[str] = mapped_column(String(64), nullable=False)
label: Mapped[str] = mapped_column(String(256), nullable=False)
group_label: Mapped[str | None] = mapped_column(String(128))
filter_type: Mapped[str] = mapped_column(String(32), nullable=False)
canonical_unit: Mapped[str | None] = mapped_column(String(32))
status: Mapped[str] = mapped_column(String(32), nullable=False)
class SourceFilterOptionRecord(Base):
__tablename__ = "source_filter_options"
__table_args__ = (UniqueConstraint("filter_id", "source_value", name="uq_filter_option"),)
id: Mapped[UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True)
filter_id: Mapped[UUID] = mapped_column(ForeignKey("source_filter_definitions.id"), nullable=False)
source_value: Mapped[str] = mapped_column(String(256), nullable=False)
normalized_value: Mapped[Any] = mapped_column(JSONB)
operator: Mapped[str | None] = mapped_column(String(16))
unit: Mapped[str | None] = mapped_column(String(32))
class SourceFilterRangeRecord(Base):
__tablename__ = "source_filter_ranges"
filter_id: Mapped[UUID] = mapped_column(ForeignKey("source_filter_definitions.id"), primary_key=True)
minimum: Mapped[Decimal | None] = mapped_column(Numeric(12, 2))
maximum: Mapped[Decimal | None] = mapped_column(Numeric(12, 2))
step: Mapped[Decimal | None] = mapped_column(Numeric(12, 2))
unit: Mapped[str | None] = mapped_column(String(32))
class CriterionDefinitionRecord(Base):
__tablename__ = "criterion_definitions"
__table_args__ = (UniqueConstraint("key", name="uq_criterion_definition_key"),)
id: Mapped[UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True)
key: Mapped[str] = mapped_column(String(128), nullable=False)
origin: Mapped[str] = mapped_column(String(16), nullable=False, default="SOURCE")
source: Mapped[str | None] = mapped_column(String(64))
product_category_id: Mapped[str | None] = mapped_column(ForeignKey("product_categories.id"))
label: Mapped[str] = mapped_column(String(256), nullable=False)
value_type: Mapped[str] = mapped_column(String(32), nullable=False)
unit: Mapped[str | None] = mapped_column(String(32))
semantic: Mapped[str | None] = mapped_column(String(128))
category: Mapped[str | None] = mapped_column(String(128))
description: Mapped[str | None] = mapped_column(Text)
scoring_attribute_key: Mapped[str | None] = mapped_column(String(128))
class SourceAttributeMappingRecord(Base):
__tablename__ = "source_attribute_mappings"
id: Mapped[UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True)
source_id: Mapped[str] = mapped_column(ForeignKey("sources.id"), nullable=False)
source_category_id: Mapped[UUID | None] = mapped_column(ForeignKey("source_categories.id"))
raw_name: Mapped[str] = mapped_column(String(256), nullable=False)
criterion_definition_id: Mapped[UUID] = mapped_column(ForeignKey("criterion_definitions.id"), nullable=False)
status: Mapped[str] = mapped_column(String(32), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
class CriterionMappingRecord(Base):
__tablename__ = "criterion_mappings"
id: Mapped[UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True)
criterion_definition_id: Mapped[UUID] = mapped_column(ForeignKey("criterion_definitions.id"), nullable=False)
criterion_key: Mapped[str] = mapped_column(String(128), nullable=False)
source_id: Mapped[str] = mapped_column(ForeignKey("sources.id"), nullable=False)
source_category_id: Mapped[UUID] = mapped_column(ForeignKey("source_categories.id"), nullable=False)
external_filter_key: Mapped[str] = mapped_column(String(64), nullable=False)
source_value: Mapped[str | None] = mapped_column(String(256))
encoding: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
status: Mapped[str] = mapped_column(String(32), nullable=False)
definition_hash: Mapped[str] = mapped_column(String(64), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
class DiscoverySearchRecord(Base):
__tablename__ = "discovery_searches"
id: Mapped[UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True)
buying_project_id: Mapped[UUID] = mapped_column(ForeignKey("buying_projects.id"), nullable=False)
source_id: Mapped[str] = mapped_column(ForeignKey("sources.id"), nullable=False)
source_category_id: Mapped[UUID] = mapped_column(ForeignKey("source_categories.id"), nullable=False)
criteria_version: Mapped[int] = mapped_column(Integer, nullable=False)
mapping_version: Mapped[str] = mapped_column(String(64), nullable=False)
generated_url: Mapped[str] = mapped_column(Text, nullable=False)
status: Mapped[str] = mapped_column(String(32), nullable=False)
schedule_interval_minutes: Mapped[int] = mapped_column(Integer, nullable=False, default=180)
broad_result_threshold: Mapped[int] = mapped_column(Integer, nullable=False, default=300)
next_run_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
last_successful_run_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
class SearchRunRecord(Base):
__tablename__ = "search_runs"
id: Mapped[UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True)
discovery_search_id: Mapped[UUID] = mapped_column(ForeignKey("discovery_searches.id"), nullable=False)
criteria_version: Mapped[int] = mapped_column(Integer, nullable=False)
mapping_version: Mapped[str] = mapped_column(String(64), nullable=False)
generated_url: Mapped[str] = mapped_column(Text, nullable=False)
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
status: Mapped[str] = mapped_column(String(32), nullable=False)
result_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
page_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
new_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
changed_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
unchanged_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
missing_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
http_status_summary: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
error_code: Mapped[str | None] = mapped_column(String(64))
class SourceProductReferenceRecord(Base):
__tablename__ = "source_product_references"
id: Mapped[UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True)
source_id: Mapped[str] = mapped_column(ForeignKey("sources.id"), nullable=False)
source_category_id: Mapped[UUID] = mapped_column(ForeignKey("source_categories.id"), nullable=False)
source_external_id: Mapped[str | None] = mapped_column(String(256))
source_url: Mapped[str | None] = mapped_column(Text)
source_title: Mapped[str] = mapped_column(String(512), nullable=False)
manufacturer: Mapped[str | None] = mapped_column(String(256))
model: Mapped[str | None] = mapped_column(String(256))
manufacturer_sku: Mapped[str | None] = mapped_column(String(128))
gtin_ean: Mapped[str | None] = mapped_column(String(32))
normalized_spec_fingerprint: Mapped[str | None] = mapped_column(Text)
first_seen_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
last_seen_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
lifecycle_status: Mapped[str] = mapped_column(String(32), nullable=False)
match_status: Mapped[str] = mapped_column(String(32), nullable=False)
consecutive_missing_success_runs: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
product_variant_id: Mapped[UUID | None] = mapped_column(ForeignKey("product_variants.id"))
class ProductObservationRecord(Base):
__tablename__ = "product_observations"
id: Mapped[UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True)
source_product_reference_id: Mapped[UUID] = mapped_column(ForeignKey("source_product_references.id"), nullable=False)
search_run_id: Mapped[UUID] = mapped_column(ForeignKey("search_runs.id"), nullable=False)
observed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
raw_title: Mapped[str] = mapped_column(String(512), nullable=False)
best_price: Mapped[Decimal | None] = mapped_column(Numeric(12, 2))
currency: Mapped[str | None] = mapped_column(String(3))
availability: Mapped[str | None] = mapped_column(String(32))
seller_count: Mapped[int | None] = mapped_column(Integer)
class ProductObservationAttributeRecord(Base):
__tablename__ = "product_observation_attributes"
__table_args__ = (UniqueConstraint("observation_id", "attribute_key", name="uq_observation_attribute"),)
id: Mapped[UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True)
observation_id: Mapped[UUID] = mapped_column(ForeignKey("product_observations.id"), nullable=False)
criterion_definition_id: Mapped[UUID | None] = mapped_column(ForeignKey("criterion_definitions.id"))
attribute_key: Mapped[str] = mapped_column(String(128), nullable=False)
raw_name: Mapped[str | None] = mapped_column(String(256))
raw_value: Mapped[Any] = mapped_column(JSONB)
normalized_value: Mapped[Any] = mapped_column(JSONB)
unit: Mapped[str | None] = mapped_column(String(32))
class SearchRunItemRecord(Base):
__tablename__ = "search_run_items"
__table_args__ = (UniqueConstraint("search_run_id", "source_product_reference_id", name="uq_run_item"),)
id: Mapped[UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True)
search_run_id: Mapped[UUID] = mapped_column(ForeignKey("search_runs.id"), nullable=False)
source_product_reference_id: Mapped[UUID] = mapped_column(ForeignKey("source_product_references.id"), nullable=False)
observation_id: Mapped[UUID | None] = mapped_column(ForeignKey("product_observations.id"))
diff_status: Mapped[str] = mapped_column(String(32), nullable=False)
class ProjectCandidateRecord(Base):
__tablename__ = "project_candidates"
__table_args__ = (
UniqueConstraint("buying_project_id", "source_product_reference_id", name="uq_project_candidate"),
)
id: Mapped[UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True)
buying_project_id: Mapped[UUID] = mapped_column(ForeignKey("buying_projects.id"), nullable=False)
source_product_reference_id: Mapped[UUID] = mapped_column(ForeignKey("source_product_references.id"), nullable=False)
product_variant_id: Mapped[UUID | None] = mapped_column(ForeignKey("product_variants.id"))
status: Mapped[str] = mapped_column(String(32), nullable=False)
match_status: Mapped[str] = mapped_column(String(32), nullable=False)
product_eligibility: Mapped[str | None] = mapped_column(String(16))
product_score: Mapped[Decimal | None] = mapped_column(Numeric(6, 2))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
class CandidateStatusHistoryRecord(Base):
__tablename__ = "candidate_status_history"
id: Mapped[UUID] = mapped_column(PGUUID(as_uuid=True), primary_key=True)
candidate_id: Mapped[UUID] = mapped_column(ForeignKey("project_candidates.id"), nullable=False)
from_status: Mapped[str | None] = mapped_column(String(32))
to_status: Mapped[str] = mapped_column(String(32), nullable=False)
reason: Mapped[str | None] = mapped_column(String(128))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)