from __future__ import annotations from dataclasses import dataclass, field from datetime import datetime from decimal import Decimal from typing import Any from uuid import UUID from app.domain.enums import ( Availability, ConstraintOperator, Eligibility, NormalizationStatus, OfferCondition, PreferenceDirection, PreferenceStrategy, PriceBasis, PriceReferenceScope, ValueType, ) @dataclass(frozen=True) class Budget: target: Decimal maximum: Decimal @dataclass(frozen=True) class Constraint: id: str attribute: str operator: ConstraintOperator value: Any = None mandatory: bool = True @dataclass(frozen=True) class Preference: id: str attribute: str strategy: PreferenceStrategy direction: PreferenceDirection acceptable: Decimal ideal: Decimal weight: Decimal @dataclass(frozen=True) class ClassificationBound: upper: Decimal inclusive: bool label: str @dataclass(frozen=True) class ScoringModel: version: str algorithm_id: str supported_preference_strategies: tuple[str, ...] minimum_confidence: Decimal maximum_price_age_hours: int optional_constraint_penalty_points: Decimal missing_preference_score: Decimal product_weight: Decimal price_weight: Decimal budget_weight: Decimal history_weight: Decimal history_window_days: int minimum_history_days: int history_discount_for_max_score: Decimal classification_upper_bounds: tuple[ClassificationBound, ...] definition: dict[str, Any] definition_hash: str @dataclass(frozen=True) class Profile: id: str version: int name: str category: str currency: str market: str allowed_conditions: tuple[OfferCondition, ...] budget: Budget scoring_version: str constraints: tuple[Constraint, ...] preferences: tuple[Preference, ...] definition: dict[str, Any] definition_hash: str @dataclass(frozen=True) class AttributeDefinition: key: str value_type: ValueType canonical_unit: str | None minimum: Decimal | None maximum: Decimal | None description: str | None @dataclass(frozen=True) class Product: id: UUID category: str brand: str name: str model: str | None created_at: datetime @dataclass(frozen=True) class ProductVariant: id: UUID product_id: UUID name: str manufacturer_sku: str | None identifiers: dict[str, Any] created_at: datetime @dataclass(frozen=True) class AttributeStatement: id: UUID variant_id: UUID attribute: str raw_value: Any normalized_value: Any unit: str | None normalization_status: NormalizationStatus normalizer_version: str confidence: Decimal source_id: str source_reference: str observed_at: datetime ingested_at: datetime normalization_error: str | None = None @dataclass(frozen=True) class Offer: id: UUID variant_id: UUID source_id: str external_id: str seller: str url: str | None market: str currency: str condition: OfferCondition created_at: datetime @dataclass(frozen=True) class PriceObservation: id: UUID offer_id: UUID source_id: str external_observation_id: str observed_at: datetime ingested_at: datetime item_price: Decimal shipping_price: Decimal | None currency: str availability: Availability raw_payload: Any confidence: Decimal @property def total_price(self) -> Decimal | None: if self.shipping_price is None: return None return self.item_price + self.shipping_price @dataclass(frozen=True) class ConstraintResult: constraint_id: str attribute: str operator: str expected: Any actual: Any status: str mandatory: bool attribute_row_id: UUID | None reason_code: str @dataclass(frozen=True) class GateResult: key: str status: str reason_code: str details: dict[str, Any] = field(default_factory=dict) @dataclass(frozen=True) class ScoreComponent: kind: str component_key: str raw_score: Decimal | None weight: Decimal | None contribution: Decimal | None details: dict[str, Any] @dataclass(frozen=True) class HistoryWindow: days: int values: tuple[Decimal, ...] observation_ids: tuple[UUID, ...] @property def day_count(self) -> int: return len(self.values) @dataclass(frozen=True) class PriceStatistics: current_total: Decimal | None median_30d: Decimal | None median_30d_days: int median_90d: Decimal | None median_90d_days: int lowest_365d: Decimal | None lowest_365d_days: int lowest_365d_is_full_year: bool used_observation_ids: tuple[UUID, ...] current_observation_id: UUID | None price_reference_scope: PriceReferenceScope = PriceReferenceScope.OFFER @dataclass(frozen=True) class Reason: code: str message: str @dataclass(frozen=True) class EvaluationResult: profile_id: str profile_version: int scoring_version: str scoring_hash: str engine_build: str variant_id: UUID offer_id: UUID as_of: datetime snapshot_cutoff: datetime product_eligibility: Eligibility offer_eligibility: Eligibility overall_eligibility: Eligibility eligible: bool | None product_score: Decimal | None product_score_raw: Decimal | None price_score: Decimal | None price_score_raw: Decimal | None deal_score: Decimal | None deal_score_raw: Decimal | None classification: str price_basis: PriceBasis | None product_coverage: Decimal constraint_results: tuple[ConstraintResult, ...] product_gates: tuple[GateResult, ...] offer_gates: tuple[GateResult, ...] components: tuple[ScoreComponent, ...] price_statistics: PriceStatistics price_reference_scope: PriceReferenceScope reasons: tuple[Reason, ...] warnings: tuple[str, ...] input_snapshot: dict[str, Any] failed_constraints: tuple[ConstraintResult, ...] unknown_constraints: tuple[ConstraintResult, ...]