import json from datetime import datetime, timezone from decimal import Decimal from pathlib import Path from uuid import UUID, uuid4 import pytest 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 ROOT = Path(__file__).resolve().parents[1] 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"), )