import json import os from datetime import datetime, timezone from decimal import Decimal from pathlib import Path from uuid import UUID, uuid4 import pytest os.environ.setdefault("GEIZHALS_USE_FIXTURES", "1") from app.domain.enums import Availability, NormalizationStatus, OfferCondition, ValueType from app.domain.models import ( AttributeDefinition, AttributeStatement, Offer, PriceObservation, Product, ProductVariant, ) from app.domain.validation import parse_profile, parse_scoring_model from app.settings import get_settings ROOT = Path(__file__).resolve().parents[1] @pytest.fixture(autouse=True) def _geizhals_fixtures(monkeypatch): monkeypatch.setenv("GEIZHALS_USE_FIXTURES", "1") get_settings.cache_clear() yield get_settings.cache_clear() AS_OF = datetime(2026, 10, 3, 12, 0, tzinfo=timezone.utc) CUTOFF = datetime(2026, 10, 3, 12, 0, tzinfo=timezone.utc) @pytest.fixture def scoring_model(): definition = json.loads((ROOT / "rules" / "generic-v1.json").read_text(encoding="utf-8")) return parse_scoring_model(definition) @pytest.fixture def definitions() -> dict[str, AttributeDefinition]: return { "ram_gb": AttributeDefinition("ram_gb", ValueType.NUMBER, "GB", Decimal("0"), None, None), "storage_gb": AttributeDefinition("storage_gb", ValueType.NUMBER, "GB", Decimal("0"), None, None), "weight_kg": AttributeDefinition("weight_kg", ValueType.NUMBER, "kg", Decimal("0"), None, None), "battery_score": AttributeDefinition("battery_score", ValueType.NUMBER, None, Decimal("0"), Decimal("100"), None), "display_score": AttributeDefinition("display_score", ValueType.NUMBER, None, Decimal("0"), Decimal("100"), None), "cpu_score": AttributeDefinition("cpu_score", ValueType.NUMBER, None, Decimal("0"), Decimal("100"), None), "ports": AttributeDefinition("ports", ValueType.STRING_LIST, None, None, None, None), "color": AttributeDefinition("color", ValueType.STRING, None, None, None, None), "has_lte": AttributeDefinition("has_lte", ValueType.BOOLEAN, None, None, None, None), } @pytest.fixture def profile(scoring_model, definitions): raw = json.loads((ROOT / "profiles" / "mobile-development-notebook.json").read_text(encoding="utf-8")) return parse_profile( raw, attribute_definitions=definitions, scoring_versions={scoring_model.version}, supported_strategies=set(scoring_model.supported_preference_strategies), ) def utc(year, month, day, hour=10, minute=0) -> datetime: return datetime(year, month, day, hour, minute, tzinfo=timezone.utc) def statement(attribute: str, value, *, confidence="1.0", status="normalized", observed=None, ingested=None, row_id=None): return AttributeStatement( id=row_id or uuid4(), variant_id=UUID("22222222-2222-4222-8222-222222222222"), attribute=attribute, raw_value=value, normalized_value=None if status != "normalized" else value, unit="GB" if attribute.endswith("_gb") else ("kg" if attribute.endswith("_kg") else None), normalization_status=NormalizationStatus(status), normalizer_version="manual-v1", confidence=Decimal(str(confidence)), source_id="manual", source_reference=f"test/{attribute}", observed_at=observed or utc(2026, 10, 3, 9), ingested_at=ingested or utc(2026, 10, 3, 9, 1), ) def product_a() -> Product: return Product( id=UUID("11111111-1111-4111-8111-111111111111"), category="notebook", brand="Example", name="Example Mobile 13", model="EM13", created_at=utc(2026, 10, 1, 8), ) def variant_a() -> ProductVariant: return ProductVariant( id=UUID("22222222-2222-4222-8222-222222222222"), product_id=UUID("11111111-1111-4111-8111-111111111111"), name="32/1024", manufacturer_sku="EM13-32-1T", identifiers={}, created_at=utc(2026, 10, 1, 8), ) def offer_a() -> Offer: return Offer( id=UUID("33333333-3333-4333-8333-333333333333"), variant_id=UUID("22222222-2222-4222-8222-222222222222"), source_id="manual", external_id="demo-offer-a", seller="DemoShop", url=None, market="DE", currency="EUR", condition=OfferCondition.NEW, created_at=utc(2026, 10, 1, 8), ) def observation(day, total, *, offer_id=None, obs_id=None, availability="in_stock", confidence="1.0", hour=10): return PriceObservation( id=obs_id or uuid4(), offer_id=offer_id or UUID("33333333-3333-4333-8333-333333333333"), source_id="manual", external_observation_id=str(obs_id or uuid4()), observed_at=utc(2026, day[0], day[1], hour) if isinstance(day, tuple) else day, ingested_at=utc(2026, 10, 3, 11), item_price=Decimal(str(total)), shipping_price=Decimal("0.00"), currency="EUR", availability=Availability(availability), raw_payload={"total": str(total)}, confidence=Decimal(str(confidence)), ) def case_a_attributes(): return ( statement("ram_gb", 32), statement("storage_gb", 1024), statement("weight_kg", Decimal("1.2")), statement("battery_score", 90), statement("display_score", 90), statement("cpu_score", 90), ) def case_a_prices(): return ( observation((9, 1), "1599.00"), observation((9, 15), "1599.00"), observation((10, 1), "1599.00"), observation((10, 3), "1298.00"), )