diff --git a/src/adcp/reporting/__init__.py b/src/adcp/reporting/__init__.py index 47fea6c52..ae2c83f1f 100644 --- a/src/adcp/reporting/__init__.py +++ b/src/adcp/reporting/__init__.py @@ -161,6 +161,7 @@ "service", "service_production", "source", + "subscriptions", "testing", } ) diff --git a/src/adcp/reporting/subscriptions/__init__.py b/src/adcp/reporting/subscriptions/__init__.py new file mode 100644 index 000000000..7eec11a06 --- /dev/null +++ b/src/adcp/reporting/subscriptions/__init__.py @@ -0,0 +1,91 @@ +"""AdCP 3.3 principal-level reporting subscriptions (experimental). + +``media_buy.reporting_subscriptions`` lets a caller register reporting once on +its principal through ``sync_principal`` instead of once per account through +``sync_accounts``. This package holds the parts both sides share: + +* :func:`expected_subscription_periods` -- the pure coverage rule. A buyer runs + it from its own records to learn exactly which periods it is owed; the + seller runtime runs it over the same facts to decide which obligations it + owes. +* :func:`resolve_subscription_offerings` -- deterministic selector resolution + (``exact`` / ``finest`` / ``all``, ties to the lowest ``offering_id``). +* :func:`subscription_capabilities` -- the capability fields a seller that + serves subscriptions must advertise. +""" + +from __future__ import annotations + +from adcp.reporting.subscriptions.capabilities import ( + REPORTING_SUBSCRIPTIONS_FEATURE, + REPORTING_SUBSCRIPTIONS_SECTION, + ReportingSubscriptionSupport, + advertise_reporting_subscriptions, + reporting_configuration_tasks, + subscription_capabilities, + with_subscription_dimensions, +) +from adcp.reporting.subscriptions.coverage import ( + AccountTimezoneChange, + ExpectedSubscriptionPeriod, + SubscriptionAuthorization, + SubscriptionBuy, + SubscriptionBuyFlight, + SubscriptionBuyPackage, + SubscriptionException, + SubscriptionVersionWindow, + expected_subscription_periods, + subscription_covers_account, + subscription_exceptions, + subscription_version_windows, +) +from adcp.reporting.subscriptions.resolution import ( + STANDARD_REPORTING_DIMENSIONS, + SubscriptionDestination, + SubscriptionMatch, + SubscriptionOffering, + SubscriptionResolution, + SubscriptionSelector, + resolve_subscription_offerings, +) +from adcp.reporting.subscriptions.wire import ( + project_offering, + project_reporting_delivery, + project_subscription_coverage, + project_subscription_state, + serves_settled_subscription_fields, +) + +__all__ = [ + "REPORTING_SUBSCRIPTIONS_FEATURE", + "REPORTING_SUBSCRIPTIONS_SECTION", + "STANDARD_REPORTING_DIMENSIONS", + "AccountTimezoneChange", + "ExpectedSubscriptionPeriod", + "ReportingSubscriptionSupport", + "SubscriptionAuthorization", + "SubscriptionBuy", + "SubscriptionBuyFlight", + "SubscriptionBuyPackage", + "SubscriptionDestination", + "SubscriptionException", + "SubscriptionMatch", + "SubscriptionOffering", + "SubscriptionResolution", + "SubscriptionSelector", + "SubscriptionVersionWindow", + "advertise_reporting_subscriptions", + "expected_subscription_periods", + "reporting_configuration_tasks", + "resolve_subscription_offerings", + "subscription_capabilities", + "subscription_covers_account", + "subscription_exceptions", + "subscription_version_windows", + "with_subscription_dimensions", + "project_offering", + "project_reporting_delivery", + "project_subscription_coverage", + "project_subscription_state", + "serves_settled_subscription_fields", +] diff --git a/src/adcp/reporting/subscriptions/_clock.py b/src/adcp/reporting/subscriptions/_clock.py new file mode 100644 index 000000000..cc7ab7be6 --- /dev/null +++ b/src/adcp/reporting/subscriptions/_clock.py @@ -0,0 +1,281 @@ +"""The period calendar shared by subscription coverage on both sides of the wire. + +``core/reporting-schedule.json`` ``period_generation``: every boundary is +calculated directly from the schedule's origin and the interval ordinal by +multiplying each ISO 8601 duration component by the ordinal and applying +years, months, days, hours, minutes, then seconds. + +* Origins: ``utc`` counts from 1970-01-01T00:00:00Z; ``account_timezone`` and + ``source_timezone`` from local midnight on 1970-01-01 in their zone; + ``billing_cycle`` from its explicit ``period_anchor`` expressed in its zone. +* Years, months and days are local civil arithmetic in the zone. The day is + clamped from the origin's day (a March 31 anchor gives February 28, then + March 31 again). A nonexistent local time advances by the gap; an ambiguous + one takes the earlier instant. +* Hours, minutes and seconds are exact elapsed time added after the civil part, + so ``PT24H`` differs from ``P1D`` across a DST change. The spec does not say + whether time components are civil; this matches the TypeScript SDK + (adcontextprotocol/adcp-client#3505), so both SDKs derive identical periods. +* A zero-length period (a skipped civil date) is never a period. +* Sub-daily local periods (adcp#8588): under ``account_timezone``, + ``source_timezone`` or ``billing_cycle`` alignment, a duration of hours, + minutes and seconds only MUST divide 24 hours, and its boundaries are the + instants whose local wall-clock time is a whole multiple of the duration + after local midnight, counted afresh each local day. A nonexistent wall time + yields no boundary and a repeated one yields one boundary per offset, so a + 23-hour DST day has 23 hourly periods and a 25-hour day 25. Local durations + cannot mix date and time components. + +The ledger's ``ReportingScheduleSpec`` treats every component as civil time and +defaults an anchorless civil schedule to the UTC epoch in local time; this +calendar does neither. +""" + +from __future__ import annotations + +import calendar as _calendar +import re +from collections.abc import Iterator +from dataclasses import dataclass, field +from datetime import date, datetime, time, timedelta, timezone +from functools import lru_cache +from zoneinfo import ZoneInfo, ZoneInfoNotFoundError, available_timezones + +UTC = timezone.utc +_DURATION = re.compile(r"P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?") +_SLA = re.compile(r"P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?") + + +def utc_instant(value: datetime, *, field: str = "timestamp") -> datetime: + """Return ``value`` in UTC, refusing naive datetimes.""" + if value.tzinfo is None or value.utcoffset() is None: + raise ValueError(f"{field} must be timezone-aware") + return value.astimezone(UTC) + + +@lru_cache(maxsize=1) +def _iana_zones() -> frozenset[str]: + return frozenset(available_timezones()) + + +def require_zone(name: str | None, *, field: str = "timezone") -> ZoneInfo: + """A recognized IANA zone; numeric offsets, unknown names and keys that are + not IANA zone names (``posixrules``, ``right/UTC``, ``posix/...``) are refused.""" + if not name or name[0] in "+-" or name[0].isdigit() or name not in _iana_zones(): + raise ValueError(f"{field} {name!r} is not a recognized IANA timezone") + try: + return ZoneInfo(name) + except (ZoneInfoNotFoundError, ValueError) as error: + raise ValueError(f"{field} {name!r} is not a recognized IANA timezone") from error + + +def parse_period_duration(value: str) -> tuple[int, int, int, int]: + """``(years, months, days, seconds)`` of a strictly positive duration.""" + match = _DURATION.fullmatch(value) + if match is None or value in {"P", "PT"} or value.endswith("T"): + raise ValueError(f"{value!r} is not an ISO 8601 period duration") + years, months, days, hours, minutes, seconds = (int(item or 0) for item in match.groups()) + exact = hours * 3600 + minutes * 60 + seconds + if not (years or months or days or exact): + raise ValueError("period_duration must be positive") + return years, months, days, exact + + +def parse_delivery_sla(value: str) -> timedelta: + """A non-negative ``delivery_sla`` as exact elapsed time (days are 24 hours). + + A ``delivery_sla`` with years or months, which the offering schema allows, + has no agreed arithmetic across SDKs and is refused. + """ + match = _SLA.fullmatch(value) + if match is None or value in {"P", "PT"} or value.endswith("T"): + raise ValueError( + f"delivery_sla {value!r} is not supported: use days, hours, minutes and " + "seconds only (years and months have no agreed arithmetic)" + ) + days, hours, minutes, seconds = (int(item or 0) for item in match.groups()) + return timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds) + + +def _resolve(local: datetime, zone: ZoneInfo) -> datetime: + """Local civil time to an instant: a gap advances, a fold takes the earlier.""" + return local.replace(tzinfo=zone, fold=0).astimezone(UTC) + + +def _add_months(local: datetime, months: int, origin_day: int) -> datetime: + index = local.year * 12 + (local.month - 1) + months + year, month = divmod(index, 12) + month += 1 + day = min(origin_day, _calendar.monthrange(year, month)[1]) + return local.replace(year=year, month=month, day=day) + + +@dataclass(frozen=True) +class PeriodCalendar: + """Ordered half-open periods of one schedule in one zone.""" + + period_duration: str + zone_name: str + origin: datetime # naive local civil time + delivery_sla: timedelta = timedelta(0) + #: Local alignment (anything but ``utc``): sub-daily durations restart at + #: each local midnight (adcp#8588). + local: bool = False + _parts: tuple[int, int, int, int] = field(init=False, repr=False, compare=False) + _zone: ZoneInfo = field(init=False, repr=False, compare=False) + _approx: float = field(init=False, repr=False, compare=False) + + def __post_init__(self) -> None: + parts = parse_period_duration(self.period_duration) + object.__setattr__(self, "_parts", parts) + object.__setattr__(self, "_zone", require_zone(self.zone_name)) + if self.origin.tzinfo is not None: + raise ValueError("origin is local civil time and must be naive") + years, months, days, exact = parts + if self.local and exact and (years or months or days): + raise ValueError( + f"period_duration {self.period_duration!r} mixes date and time components, " + "which a local alignment does not allow" + ) + if self.local and exact and not (years or months or days) and 86400 % exact: + raise ValueError( + f"period_duration {self.period_duration!r} must divide 24 hours " + "under a local alignment" + ) + approx = years * 365.2425 * 86400 + months * 30.436875 * 86400 + days * 86400 + exact + object.__setattr__(self, "_approx", approx) + + @classmethod + def for_schedule( + cls, + *, + period_duration: str, + delivery_sla: str, + zone_name: str, + anchor: datetime | None = None, + local: bool = False, + ) -> PeriodCalendar: + """``anchor`` is an instant (``billing_cycle``); otherwise local midnight 1970-01-01. + + ``local`` marks an ``account_timezone``, ``source_timezone`` or + ``billing_cycle`` schedule. + """ + zone = require_zone(zone_name) + origin = ( + utc_instant(anchor, field="period_anchor").astimezone(zone).replace(tzinfo=None) + if anchor is not None + else datetime(1970, 1, 1) + ) + return cls( + period_duration, zone_name, origin, parse_delivery_sla(delivery_sla), local=local + ) + + @property + def _subdaily_local(self) -> bool: + years, months, days, exact = self._parts + return self.local and bool(exact) and not (years or months or days) + + def _day_boundaries(self, day: date) -> list[datetime]: + """Every boundary on one local date: wall-clock multiples after local midnight.""" + step = self._parts[3] + found: set[datetime] = set() + midnight = datetime.combine(day, time()) + for index in range(86400 // step): + wall = midnight + timedelta(seconds=index * step) + for fold in (0, 1): + instant = wall.replace(tzinfo=self._zone, fold=fold).astimezone(UTC) + if instant.astimezone(self._zone).replace(tzinfo=None) == wall: + found.add(instant) + return sorted(found) + + def _boundaries_from(self, instant: datetime) -> Iterator[datetime]: + """Sub-daily local boundaries at or after ``instant``, in order.""" + at = utc_instant(instant) + day = at.astimezone(self._zone).date() - timedelta(days=1) + while True: + for boundary in self._day_boundaries(day): + if boundary >= at: + yield boundary + day += timedelta(days=1) + + def boundary(self, ordinal: int) -> datetime: + years, months, days, exact = self._parts + local = self.origin + if years or months: + local = _add_months(local, (years * 12 + months) * ordinal, self.origin.day) + if days: + local = local + timedelta(days=days * ordinal) + return _resolve(local, self._zone) + timedelta(seconds=exact * ordinal) + + def bounds(self, ordinal: int) -> tuple[datetime, datetime]: + return self.boundary(ordinal), self.boundary(ordinal + 1) + + def first_ordinal_at_or_after(self, instant: datetime) -> int: + at = utc_instant(instant) + origin = self.boundary(0) + ordinal = int((at - origin).total_seconds() // self._approx) + while self.boundary(ordinal) < at: + ordinal += 1 + while self.boundary(ordinal - 1) >= at: + ordinal -= 1 + return ordinal + + def ceil(self, instant: datetime) -> datetime: + """The first boundary at or after ``instant`` (E for a cutover at it).""" + if self._subdaily_local: + return next(self._boundaries_from(instant)) + return self.boundary(self.first_ordinal_at_or_after(instant)) + + def floor(self, instant: datetime) -> datetime: + """The last boundary at or before ``instant``.""" + at = utc_instant(instant) + if self._subdaily_local: + day = at.astimezone(self._zone).date() + timedelta(days=1) + while True: + earlier = [item for item in self._day_boundaries(day) if item <= at] + if earlier: + return earlier[-1] + day -= timedelta(days=1) + ordinal = self.first_ordinal_at_or_after(at) + start = self.boundary(ordinal) + return start if start == at else self.boundary(ordinal - 1) + + def periods( + self, *, starts_from: datetime, starts_before: datetime + ) -> Iterator[tuple[datetime, datetime]]: + """Every nonempty period whose start lies in ``[starts_from, starts_before)``.""" + upper = utc_instant(starts_before) + if self._subdaily_local: + boundaries = self._boundaries_from(starts_from) + start = next(boundaries) + while start < upper: + end = next(boundaries) + yield start, end + start = end + return + ordinal = self.first_ordinal_at_or_after(starts_from) + start = self.boundary(ordinal) + while start < upper: + end = self.boundary(ordinal + 1) + if end > start: + yield start, end + ordinal += 1 + start = end + + +def iso_z(value: datetime) -> str: + """RFC 3339 UTC with a ``Z`` suffix and no fractional seconds when whole.""" + instant = utc_instant(value) + if instant.microsecond: + return instant.isoformat().replace("+00:00", "Z") + return instant.strftime("%Y-%m-%dT%H:%M:%SZ") + + +__all__ = [ + "PeriodCalendar", + "iso_z", + "parse_delivery_sla", + "parse_period_duration", + "require_zone", + "utc_instant", +] diff --git a/src/adcp/reporting/subscriptions/capabilities.py b/src/adcp/reporting/subscriptions/capabilities.py new file mode 100644 index 000000000..1fdc1689d --- /dev/null +++ b/src/adcp/reporting/subscriptions/capabilities.py @@ -0,0 +1,200 @@ +"""Capability fields a seller serving reporting subscriptions advertises. + +``core/reporting-delivery-capabilities.json`` (3.3): + +* ``configuration_tasks`` lists every task that accepts reporting + registrations: ``sync_principal`` for subscriptions, ``sync_accounts`` for + account-level ``reporting_delivery_configs``. +* The deprecated ``configuration_task: "sync_accounts"`` is present when and + only when ``configuration_tasks`` lists ``sync_accounts`` or is absent. +* ``subscriptions`` (``supported``, optional ``max_accounts``, + ``backfill_on_regrant``, ``dimensions``) is present when and only when + ``configuration_tasks`` lists ``sync_principal``. +* Advertising subscriptions also requires the ``media_buy.reporting_subscriptions`` + experimental feature and ``reporting_subscriptions`` in + ``adcp.principal.supported_sections``. + +A seller that does not serve subscriptions keeps its existing block: these +helpers leave it byte-for-byte unchanged when ``support`` is ``None``. +""" + +from __future__ import annotations + +import re +from collections.abc import Iterable, Mapping +from dataclasses import dataclass +from typing import Any + +from adcp.reporting.subscriptions.resolution import STANDARD_REPORTING_DIMENSIONS + +#: Experimental feature id for principal-level reporting subscriptions. +REPORTING_SUBSCRIPTIONS_FEATURE = "media_buy.reporting_subscriptions" +#: ``adcp.principal.supported_sections`` value for the section. +REPORTING_SUBSCRIPTIONS_SECTION = "reporting_subscriptions" +_DIMENSION = re.compile(r"[a-z][a-z0-9_]*") + + +@dataclass(frozen=True) +class ReportingSubscriptionSupport: + """What a seller declares about its reporting-subscription runtime. + + ``account_configuration`` keeps ``sync_accounts`` in ``configuration_tasks`` + (and the deprecated ``configuration_task``) for sellers that still serve + account-level registration; set it ``False`` only when the seller accepts + reporting registration through ``sync_principal`` alone, which serves 3.3 + callers only. + """ + + max_accounts: int | None = None + backfill_on_regrant: bool = False + dimensions: tuple[str, ...] = () + account_configuration: bool = True + + def __post_init__(self) -> None: + if self.max_accounts is not None and self.max_accounts < 1: + raise ValueError("max_accounts must be at least 1") + dimensions = tuple(dict.fromkeys(self.dimensions)) + for value in dimensions: + if not _DIMENSION.fullmatch(value): + raise ValueError(f"dimension {value!r} is not a vocabulary token") + object.__setattr__(self, "dimensions", dimensions) + + @property + def standard_dimensions(self) -> tuple[str, ...]: + return tuple(item for item in self.dimensions if item in STANDARD_REPORTING_DIMENSIONS) + + def block(self) -> dict[str, Any]: + """The ``reporting_delivery.subscriptions`` capability object.""" + payload: dict[str, Any] = {"supported": True} + if self.max_accounts is not None: + payload["max_accounts"] = self.max_accounts + if self.backfill_on_regrant: + payload["backfill_on_regrant"] = True + if self.dimensions: + payload["dimensions"] = list(self.dimensions) + return payload + + +def reporting_configuration_tasks( + *, subscriptions: bool, account_configuration: bool = True +) -> list[str]: + """``configuration_tasks`` in the schema's order of preference.""" + tasks: list[str] = [] + if subscriptions: + tasks.append("sync_principal") + if account_configuration: + tasks.append("sync_accounts") + if not tasks: + raise ValueError("a reporting seller accepts registration through at least one task") + return tasks + + +def with_subscription_dimensions(offering: Mapping[str, Any] | Any, mapping: Any) -> dict[str, Any]: + """An offering's wire form with ``standard_dimensions`` and ``unmapped_dimensions``. + + ``mapping`` is the offering's + :class:`~adcp.reporting.subscriptions.SubscriptionOffering` (or anything + with ``dimensions`` and ``unmapped_dimensions``). Sellers advertising + ``media_buy.reporting_subscriptions`` MUST publish ``standard_dimensions`` + on every offering (adcp#8585); ``unmapped_dimensions`` appears only when + any exist. Values are sorted so the declaration is stable. Serve the block + through :func:`~adcp.reporting.subscriptions.wire.project_reporting_delivery` + so callers pinned below 3.3.0-beta.2 do not receive these fields. + """ + payload: dict[str, Any] = ( + dict(offering) + if isinstance(offering, Mapping) + else offering.model_dump(mode="json", exclude_none=True) + ) + payload["standard_dimensions"] = sorted(getattr(mapping, "dimensions")) + unmapped = sorted(getattr(mapping, "unmapped_dimensions", ()) or ()) + if unmapped: + payload["unmapped_dimensions"] = unmapped + else: + payload.pop("unmapped_dimensions", None) + return payload + + +def subscription_capabilities( + reporting_delivery: Mapping[str, Any], support: ReportingSubscriptionSupport | None +) -> dict[str, Any]: + """Return ``reporting_delivery`` with the registration fields for ``support``. + + ``None`` returns an unchanged copy, so a seller without subscriptions + advertises exactly what it did before. With ``support``, every offering + must already carry ``standard_dimensions`` (see + :func:`with_subscription_dimensions`). + """ + payload = dict(reporting_delivery) + if support is None: + return payload + missing = [ + str(item.get("offering_id")) + for item in payload.get("offerings") or () + if "standard_dimensions" not in item + ] + if missing: + raise ValueError( + "sellers advertising reporting subscriptions must publish standard_dimensions " + f"on every offering; missing on {missing}" + ) + tasks = reporting_configuration_tasks( + subscriptions=True, account_configuration=support.account_configuration + ) + payload["configuration_tasks"] = tasks + if "sync_accounts" in tasks: + payload["configuration_task"] = "sync_accounts" + else: + payload.pop("configuration_task", None) + payload["subscriptions"] = support.block() + return payload + + +def _append_unique(values: Iterable[Any], value: str) -> list[str]: + result = [str(getattr(item, "value", item)) for item in values] + if value not in result: + result.append(value) + return result + + +def advertise_reporting_subscriptions( + capabilities: Mapping[str, Any], support: ReportingSubscriptionSupport | None +) -> dict[str, Any]: + """Add the feature id and principal section to a capabilities response. + + Expects ``media_buy.reporting_delivery`` already projected through + :func:`subscription_capabilities`. A seller advertising subscriptions must + already advertise the principal layer, so an absent ``adcp.principal`` + block raises rather than producing the feature id without the section. + """ + payload = dict(capabilities) + if support is None: + return payload + payload["experimental_features"] = _append_unique( + payload.get("experimental_features") or (), REPORTING_SUBSCRIPTIONS_FEATURE + ) + adcp = payload.get("adcp") + if not (isinstance(adcp, Mapping) and isinstance(adcp.get("principal"), Mapping)): + # The feature id without the reporting_subscriptions section is a + # non-conformant advertisement. + raise ValueError( + "advertising reporting subscriptions requires the adcp.principal block, which " + "carries the reporting_subscriptions section" + ) + principal = dict(adcp["principal"]) + principal["supported_sections"] = _append_unique( + principal.get("supported_sections") or (), REPORTING_SUBSCRIPTIONS_SECTION + ) + payload["adcp"] = {**adcp, "principal": principal} + return payload + + +__all__ = [ + "REPORTING_SUBSCRIPTIONS_FEATURE", + "REPORTING_SUBSCRIPTIONS_SECTION", + "ReportingSubscriptionSupport", + "advertise_reporting_subscriptions", + "reporting_configuration_tasks", + "subscription_capabilities", + "with_subscription_dimensions", +] diff --git a/src/adcp/reporting/subscriptions/coverage.py b/src/adcp/reporting/subscriptions/coverage.py new file mode 100644 index 000000000..8d376204a --- /dev/null +++ b/src/adcp/reporting/subscriptions/coverage.py @@ -0,0 +1,1008 @@ +"""Which reporting periods a subscription owes one account: the buyer's rule. + +This is the pure, plain-data form of the coverage rule in +``core/reporting-subscription.json`` (``x-adcp-validation.coverage`` and +``cutover``). A buyer runs it from its own records; the seller runtime runs the +same function over the same facts to decide which obligations it owes, so the +two sides agree period by period by construction. + +For each covered buy and resolved offering, the owed periods are the complete +periods of the offering's published schedule that + +1. start on or after the first boundary at or after the later of the + subscription version's ``accepted_at`` and the account's ``authorized_at``, + and end after the buy's ``confirmed_at`` (adcp#8593: the confirmation + period is owed in full); and +2. overlap the buy's flight from ``start_time`` to its stop time, the earliest + of ``end_time``, ``canceled_at`` and ``completed_at``, with the flight taken + as of the period's ``scope_resolved_at`` (its end). + +Periods are never clipped: the period containing the stop time is owed in +full. Every change that hands periods from an outgoing schedule to an incoming +one at an instant T is a cutover: the outgoing schedule owes its periods that +start before T in full, E is the end of the last of them, and the incoming +schedule owes its periods that start at or after E. Version changes, +deactivation, resolution changes and account-timezone changes all use it. + +Exceptions only remove periods: each ``excluded[]`` entry and each ``gaps[]`` +entry removes the periods whose start falls in ``[from, until)``. Lost +authority removes periods the same way unless ``backfill_on_regrant`` keeps +them. + +Rules settled by adcp#8585 (identical in the TypeScript SDK) + Time is split per buy at every version ``accepted_at`` / ``superseded_at``, + deactivation and reactivation instant, offering ``available_from`` / + ``withdrawn_at`` and account timezone cutover. Each stretch with an active, + in-scope version and an unchanged resolution (the union over the buy's + packages) is a regime. An offering continuing into the next regime hands + off at E of its own outgoing calendar; a newly resolved offering starts at + the latest E across everything the previous regime resolved; after a regime + that resolved nothing, or no active version, the ordinary start rule applies. + A re-grant before ``unauthorized_at + status_retention_days`` continues the + authority chain; a later one starts a new chain. Reactivation is a fresh + start (``inactive_intervals``), and ``gaps[]`` and ``timezone_cutovers[]`` + entries with an ``offering_id`` apply to that offering only. +""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping, Sequence +from dataclasses import dataclass, field, replace +from datetime import datetime, timedelta +from typing import Any + +from adcp.reporting._reconcile import ExpectedReportingPeriod +from adcp.reporting.subscriptions._clock import PeriodCalendar, iso_z, require_zone, utc_instant +from adcp.reporting.subscriptions.resolution import ( + SubscriptionOffering, + SubscriptionSelector, + _text, + _value, + resolve_subscription_offerings, +) + +#: Exclusion reasons that remove periods. ``package_ids`` entries never do. +_REASONS = frozenset( + {"offering_unsupported", "capacity", "overridden", "excluded_by_buyer", "action_required"} +) + + +def _instant(value: Any, *, field: str) -> datetime | None: + if value is None: + return None + if isinstance(value, str): + value = datetime.fromisoformat(value.replace("Z", "+00:00")) + if not isinstance(value, datetime): + raise TypeError(f"{field} must be a datetime or an RFC 3339 string") + return utc_instant(value, field=field) + + +def _require_instant(value: Any, *, field: str) -> datetime: + instant = _instant(value, field=field) + if instant is None: + raise ValueError(f"{field} is required") + return instant + + +# -- plain-data inputs ------------------------------------------------------ + + +@dataclass(frozen=True) +class SubscriptionBuyFlight: + """A flight a buy had before a later change superseded it at ``superseded_at``. + + Periods whose ``scope_resolved_at`` (end) is before ``superseded_at`` were + resolved under this flight, so an extension or reinstatement never makes + an elapsed period owed after the fact. + """ + + superseded_at: datetime + start_time: datetime + end_time: datetime + canceled_at: datetime | None = None + completed_at: datetime | None = None + + def __post_init__(self) -> None: + for name in ("superseded_at", "start_time", "end_time", "canceled_at", "completed_at"): + value = getattr(self, name) + if value is not None: + object.__setattr__(self, name, utc_instant(value, field=name)) + + @property + def stop_time(self) -> datetime: + return _stop(self.end_time, self.canceled_at, self.completed_at) + + +def _stop( + end_time: datetime, canceled_at: datetime | None, completed_at: datetime | None +) -> datetime: + return min(item for item in (end_time, canceled_at, completed_at) if item is not None) + + +@dataclass(frozen=True) +class SubscriptionBuyPackage: + """One package of a buy and its product's ``reporting_delivery_offering_ids``.""" + + package_id: str + reporting_delivery_offering_ids: tuple[str, ...] + + def __post_init__(self) -> None: + object.__setattr__( + self, + "reporting_delivery_offering_ids", + tuple(self.reporting_delivery_offering_ids), + ) + + +@dataclass(frozen=True) +class SubscriptionBuy: + """One media buy the caller can see through ``get_media_buys``, as plain data. + + The top-level flight is the buy's current one. ``superseded_flights`` + records earlier flights and when a change replaced each. Without + ``packages``, ``reporting_delivery_offering_ids`` is the buy's products' + offering ids, resolved as one product. + """ + + media_buy_id: str + confirmed_at: datetime | None + start_time: datetime + end_time: datetime + canceled_at: datetime | None = None + completed_at: datetime | None = None + packages: tuple[SubscriptionBuyPackage, ...] = () + reporting_delivery_offering_ids: tuple[str, ...] = () + superseded_flights: tuple[SubscriptionBuyFlight, ...] = () + + def __post_init__(self) -> None: + if not self.media_buy_id: + raise ValueError("media_buy_id is required") + for name in ("confirmed_at", "start_time", "end_time", "canceled_at", "completed_at"): + value = getattr(self, name) + if value is not None: + object.__setattr__(self, name, utc_instant(value, field=name)) + object.__setattr__(self, "packages", tuple(self.packages)) + object.__setattr__( + self, "reporting_delivery_offering_ids", tuple(self.reporting_delivery_offering_ids) + ) + flights = tuple(sorted(self.superseded_flights, key=lambda item: item.superseded_at)) + object.__setattr__(self, "superseded_flights", flights) + + @property + def stop_time(self) -> datetime: + return _stop(self.end_time, self.canceled_at, self.completed_at) + + def flight_at(self, resolved_at: datetime) -> tuple[datetime, datetime]: + """``(start_time, stop_time)`` as of a period's ``scope_resolved_at``. + + A change effective exactly at ``resolved_at`` is visible. + """ + for flight in self.superseded_flights: + if resolved_at < flight.superseded_at: + return flight.start_time, flight.stop_time + return self.start_time, self.stop_time + + def product_offering_ids(self) -> tuple[tuple[str, tuple[str, ...]], ...]: + if self.packages: + return tuple( + (package.package_id, package.reporting_delivery_offering_ids) + for package in self.packages + ) + return (("", self.reporting_delivery_offering_ids),) + + def _earliest_start(self) -> datetime: + return min((self.start_time, *(flight.start_time for flight in self.superseded_flights))) + + def _latest_stop(self) -> datetime: + return max((self.stop_time, *(flight.stop_time for flight in self.superseded_flights))) + + +@dataclass(frozen=True) +class SubscriptionAuthorization: + """One grant of account authority to the caller, ``[authorized_at, unauthorized_at)``. + + Pass the account's grant history, oldest first. A loss followed by a + re-grant within ``status_retention_days`` continues coverage, with a gap + unless the seller backfills; a later re-grant is a fresh coverage start. + A grant whose ``authorized_at`` is at or before the previous loss, such as + one listed with the chain-start ``authorized_at`` that ``list_accounts`` + keeps after a re-grant inside retention (adcp#8585), continues the chain + at an unknown instant: the seller's ``gaps[]`` say which periods it removed. + """ + + authorized_at: datetime + unauthorized_at: datetime | None = None + + def __post_init__(self) -> None: + object.__setattr__( + self, "authorized_at", utc_instant(self.authorized_at, field="authorized_at") + ) + if self.unauthorized_at is not None: + object.__setattr__( + self, + "unauthorized_at", + utc_instant(self.unauthorized_at, field="unauthorized_at"), + ) + if self.unauthorized_at <= self.authorized_at: + raise ValueError("unauthorized_at must be after authorized_at") + + +@dataclass(frozen=True) +class AccountTimezoneChange: + """An account timezone change at ``changed_at`` (the instant T of the cutover). + + ``offering_id`` limits it to one offering, as a ``timezone_cutovers[]`` + entry with ``offering_id`` does; ``previous_timezone`` is the zone before + the change, which every seller entry carries and the coverage rule + requires. + """ + + changed_at: datetime + timezone: str + offering_id: str | None = None + previous_timezone: str | None = None + + def __post_init__(self) -> None: + object.__setattr__(self, "changed_at", utc_instant(self.changed_at, field="changed_at")) + + +@dataclass(frozen=True) +class SubscriptionException: + """One period-removing exception: periods starting in ``[from_, until)``. + + ``media_buy_id`` scopes it to one buy; otherwise it applies to the account. + """ + + reason: str + from_: datetime + until: datetime | None = None + media_buy_id: str | None = None + #: A ``gaps[]`` entry for one offering's boundaries; ``None`` is every offering. + offering_id: str | None = None + + def __post_init__(self) -> None: + object.__setattr__(self, "from_", utc_instant(self.from_, field="from")) + if self.until is not None: + object.__setattr__(self, "until", utc_instant(self.until, field="until")) + + def removes(self, start: datetime, media_buy_id: str, offering_id: str | None = None) -> bool: + if self.media_buy_id is not None and self.media_buy_id != media_buy_id: + return False + if self.offering_id is not None and offering_id is not None: + if self.offering_id != offering_id: + return False + return self.from_ <= start and (self.until is None or start < self.until) + + +def subscription_exceptions( + excluded: Iterable[Any] = (), gaps: Iterable[Any] = () +) -> tuple[SubscriptionException, ...]: + """Normalize ``subscription_coverage`` ``excluded[]`` and ``gaps[]`` entries. + + An ``offering_unsupported`` entry naming ``package_ids`` removes those + packages from the buy's coverage, not periods, so it is dropped here. + """ + result: list[SubscriptionException] = [] + for entry in excluded: + if isinstance(entry, SubscriptionException): + result.append(entry) + continue + if hasattr(entry, "model_dump"): + entry = entry.model_dump(mode="json", exclude_none=True, by_alias=True) + if _value(entry, "package_ids"): + continue + reason = _text(_value(entry, "reason")) + if reason not in _REASONS: + raise ValueError(f"unknown subscription exclusion reason {reason!r}") + media_buy_id = _value(entry, "media_buy_id") + result.append( + SubscriptionException( + reason=reason, + from_=_require_instant(_value(entry, "from", _value(entry, "from_")), field="from"), + until=_instant(_value(entry, "until"), field="until"), + media_buy_id=None if media_buy_id is None else _text(media_buy_id), + ) + ) + for gap in gaps: + if hasattr(gap, "model_dump"): + gap = gap.model_dump(mode="json", exclude_none=True, by_alias=True) + offering_id = _value(gap, "offering_id") + result.append( + SubscriptionException( + reason="unauthorized", + from_=_require_instant(_value(gap, "from", _value(gap, "from_")), field="from"), + until=_require_instant(_value(gap, "until"), field="until"), + offering_id=None if offering_id is None else _text(offering_id), + ) + ) + return tuple(result) + + +@dataclass(frozen=True) +class SubscriptionVersionWindow: + """One subscription version and the instants it governs: ``[accepted_at, ends_at)``. + + ``ends_at`` is the next version's ``accepted_at`` (``superseded_at``) or the + ``deactivated_at`` of the last version; ``None`` while it is current. + """ + + subscription_version: int + accepted_at: datetime + ends_at: datetime | None + configuration: Any + #: ``(from, until)`` stretches during which the version was inactive in + #: place (``active: false`` then ``true`` without a new version); ``until`` + #: ``None`` is still inactive. Reactivation is a fresh start at ``until``. + inactive_intervals: tuple[tuple[datetime, datetime | None], ...] = () + #: The pattern and transport of the destination this version names. + destination: Any | None = None + + def __post_init__(self) -> None: + object.__setattr__(self, "accepted_at", utc_instant(self.accepted_at, field="accepted_at")) + if self.ends_at is not None: + object.__setattr__(self, "ends_at", utc_instant(self.ends_at, field="ends_at")) + if self.ends_at < self.accepted_at: + raise ValueError("a version cannot end before it was accepted") + intervals = [] + for start, until in self.inactive_intervals: + start = utc_instant(start, field="inactive_intervals.from") + if until is not None: + until = utc_instant(until, field="inactive_intervals.until") + if until <= start: + raise ValueError("an inactive interval must end after it starts") + intervals.append((start, until)) + object.__setattr__(self, "inactive_intervals", tuple(intervals)) + + def active_at(self, instant: datetime) -> bool: + if not (self.accepted_at <= instant and (self.ends_at is None or instant < self.ends_at)): + return False + return not any( + start <= instant and (until is None or instant < until) + for start, until in self.inactive_intervals + ) + + def contains(self, instant: datetime) -> bool: + return self.active_at(instant) + + +def subscription_version_windows( + subscription: Any, + *, + version_configurations: Mapping[int, Any] | None = None, + require_prior_configurations: bool = False, +) -> tuple[SubscriptionVersionWindow, ...]: + """Derive version windows from a ``core/reporting-subscription-state.json`` readback. + + Each prior version's configuration comes from ``version_configurations`` + when given, else from the readback's ``prior_versions[].configuration`` + (adcp#8588). A seller on 3.3.0-beta.1 does not send it; then the current + configuration is assumed, unless ``require_prior_configurations`` makes + the gap an error. A version accepted while the subscription was already + inactive has an empty window. + """ + if hasattr(subscription, "model_dump"): + subscription = subscription.model_dump(mode="json", exclude_none=True) + configurations = dict(version_configurations or {}) + current_version = int(_value(subscription, "subscription_version")) + current_configuration = _value(subscription, "configuration") + if current_configuration is None: + raise ValueError("subscription state has no configuration") + state = _text(_value(subscription, "state", "active")) + deactivated_at = _instant(_value(subscription, "deactivated_at"), field="deactivated_at") + if state == "inactive" and deactivated_at is None: + raise ValueError("an inactive subscription must carry deactivated_at") + accepted_at = _require_instant(_value(subscription, "accepted_at"), field="accepted_at") + windows = [ + SubscriptionVersionWindow( + subscription_version=current_version, + accepted_at=accepted_at, + ends_at=( + max(accepted_at, deactivated_at) + if state == "inactive" and deactivated_at is not None + else None + ), + configuration=configurations.get(current_version, current_configuration), + ) + ] + for prior in _value(subscription, "prior_versions", ()) or (): + version = int(_value(prior, "subscription_version")) + configuration = configurations.get(version, _value(prior, "configuration")) + if configuration is None: + if require_prior_configurations: + raise ValueError( + f"no configuration is known for prior subscription_version {version}" + ) + configuration = current_configuration + windows.append( + SubscriptionVersionWindow( + subscription_version=version, + accepted_at=_require_instant(_value(prior, "accepted_at"), field="accepted_at"), + ends_at=_require_instant(_value(prior, "superseded_at"), field="superseded_at"), + configuration=configuration, + ) + ) + inactive = tuple( + ( + _require_instant(_value(item, "deactivated_at"), field="deactivated_at"), + _instant(_value(item, "reactivated_at"), field="reactivated_at"), + ) + for item in _value(subscription, "inactive_intervals", ()) or () + ) + if inactive: + # The readback lists them per subscription; they apply to whichever + # version was current, so every window carries them. + windows = [replace(window, inactive_intervals=inactive) for window in windows] + windows.sort(key=lambda item: (item.accepted_at, item.subscription_version)) + for earlier, later in zip(windows, windows[1:]): + if earlier.subscription_version >= later.subscription_version: + raise ValueError("subscription versions must increase with accepted_at") + if earlier.ends_at != later.accepted_at: + raise ValueError("superseded_at must equal the next version's accepted_at") + return tuple(windows) + + +def subscription_covers_account( + configuration: Any, *, account_id: str, account_operator: str | None = None +) -> bool: + """Whether a subscription version's scope selects the account. + + ``exclude_account_ids`` and ``operator_domains`` narrow the scope; the + seller also reports an excluded account as ``excluded_by_buyer``. With + ``operator_domains``, an account whose operator is unknown is not selected. + """ + if hasattr(configuration, "model_dump"): + configuration = configuration.model_dump(mode="json", exclude_none=True) + scope = _value(configuration, "scope") or {} + account_ids = _value(scope, "account_ids") + if account_ids is not None: + if account_id not in {_text(item) for item in account_ids}: + return False + elif _value(scope, "all_authorized_accounts") is not True: + return False + excluded = _value(configuration, "exclude_account_ids") or () + if account_id in {_text(item) for item in excluded}: + return False + domains = _value(configuration, "operator_domains") + if domains: + return account_operator is not None and account_operator in { + _text(item) for item in domains + } + return True + + +# -- output ----------------------------------------------------------------- + + +@dataclass(frozen=True, kw_only=True) +class ExpectedSubscriptionPeriod(ExpectedReportingPeriod): + """One owed period of one (subscription version, resolved offering). + + An :class:`~adcp.reporting.ExpectedReportingPeriod`, so it feeds + :func:`~adcp.reporting.evaluate_reporting_ledger` and the consumer-status + planner unchanged. ``delivery_config_id`` is the ``subscription_id`` and + ``delivery_config_version`` the ``subscription_version``. ``media_buy_ids`` + are the buys owed this period under this offering, the denominator the + seller freezes at ``scope_resolved_at``. + + ``withheld`` marks a period owed before a loss of authority that is not + due while authority is absent; its ``expected_at`` is ``None``. After a + re-grant it is due at the re-grant time plus the offering's + ``delivery_sla``, never earlier than its original ``expected_at``. + """ + + account_id: str + subscription_id: str + offering_id: str + withheld: bool = False + + +# -- the rule ---------------------------------------------------------------- + + +@dataclass(frozen=True) +class _Resolved: + offering: SubscriptionOffering + calendar: PeriodCalendar + + @property + def key(self) -> tuple[str, str, datetime]: + return (self.offering.offering_id, self.calendar.zone_name, self.calendar.origin) + + +@dataclass +class _Regime: + start: datetime + until: datetime | None + version: int + resolved: tuple[_Resolved, ...] + #: Per offering: the instant from which this regime owes periods (E, or + #: the regime start). Carried so a later cutover inside the same outgoing + #: period never owes a period twice. + handoffs: dict[str, datetime] = field(default_factory=dict) + + +@dataclass +class _Chain: + """Continuous authority: a first grant, retained interruptions, an optional end.""" + + start: datetime + #: (lost, regranted); regranted is None when only the chain-start + #: authorized_at says authority returned (the re-grant instant is unknown). + interruptions: list[tuple[datetime, datetime | None]] = field(default_factory=list) + end: datetime | None = None + #: The re-grant that started this chain after an expired entry, if any. + after_expiry_of: datetime | None = None + + +@dataclass +class _Owed: + version: int + offering: SubscriptionOffering + zone: str + start: datetime + end: datetime + expected_at: datetime | None + withheld: bool + media_buy_ids: set[str] = field(default_factory=set) + + +def _chains( + authorizations: Sequence[SubscriptionAuthorization], retention: timedelta | None +) -> list[_Chain]: + chains: list[_Chain] = [] + previous_end: datetime | None = None + for index, grant in enumerate(authorizations): + if index > 0 and previous_end is None: + raise ValueError("only the last grant may be unrevoked") + if chains and previous_end is not None and grant.authorized_at <= previous_end: + # adcp#8585 keeps authorized_at at the chain start after a re-grant + # inside retention, so a grant listed with it continues the chain + # at an unknown instant; the seller's gaps[] says what it removed. + chains[-1].interruptions.append((previous_end, None)) + chains[-1].end = None + elif chains and previous_end is not None: + if retention is None: + raise ValueError("status_retention_days is required to evaluate a re-grant") + if grant.authorized_at < previous_end + retention: + chains[-1].interruptions.append((previous_end, grant.authorized_at)) + chains[-1].end = None + else: + chains.append(_Chain(grant.authorized_at, after_expiry_of=previous_end)) + else: + chains.append(_Chain(grant.authorized_at)) + if grant.unauthorized_at is not None: + chains[-1].end = grant.unauthorized_at + previous_end = grant.unauthorized_at + return chains + + +def _authorized( + chains: Sequence[_Chain], calendar: PeriodCalendar, start: datetime, backfill: bool +) -> bool: + if not chains: + return True # authorized_at is not advertised and authority never lapsed + for chain in chains: + if start < calendar.ceil(chain.start): + continue + if chain.end is not None and start >= calendar.floor(chain.end): + continue + if not backfill and any( + calendar.floor(lost) <= start < calendar.ceil(regranted) + for lost, regranted in chain.interruptions + if regranted is not None + ): + continue + return True + return False + + +def _expected_at( + chains: Sequence[_Chain], + calendar: PeriodCalendar, + start: datetime, + end: datetime, + backfill: bool, +) -> tuple[datetime | None, bool]: + """When a period is due, and whether it is withheld for lost authority.""" + sla = calendar.delivery_sla + expected: datetime = end + sla + for chain in chains: + for lost, regranted in chain.interruptions: + if regranted is None: + continue # an undated re-grant: the original deadline stands + pending = end <= calendar.floor(lost) and expected > lost + backfilled = backfill and calendar.floor(lost) <= start < calendar.ceil(regranted) + if pending or backfilled: + expected = max(expected, regranted + sla) + if chain.end is not None and end <= calendar.floor(chain.end) and expected > chain.end: + return None, True + return expected, False + + +def _timezone_at( + instant: datetime, + current: str | None, + changes: Sequence[AccountTimezoneChange], + offering_id: str, +) -> str | None: + """The zone an offering's account-timezone periods use at ``instant``. + + Only changes that apply to the offering count. Before the first of them + the zone is that change's ``previous_timezone``; with none, it is the + account's current zone throughout (adcp#8585). + """ + applicable = [item for item in changes if item.offering_id in {None, offering_id}] + if not applicable: + return current + zone = applicable[0].previous_timezone + for change in applicable: + if change.changed_at <= instant: + zone = change.timezone + return zone + + +def _windows( + subscription: Any, version_configurations: Mapping[int, Any] | None +) -> tuple[SubscriptionVersionWindow, ...]: + if isinstance(subscription, Sequence) and not isinstance(subscription, (str, bytes, Mapping)): + windows = tuple(subscription) + for earlier, later in zip(windows, windows[1:]): + if later.subscription_version <= earlier.subscription_version or ( + earlier.ends_at != later.accepted_at + ): + raise ValueError("versions must be ordered and each superseded at the next") + return windows + return subscription_version_windows(subscription, version_configurations=version_configurations) + + +def expected_subscription_periods( + *, + subscription: Any, + account_id: str, + buys: Sequence[SubscriptionBuy], + offerings: Sequence[SubscriptionOffering], + account_timezone: str | None, + now: datetime, + authorizations: Sequence[SubscriptionAuthorization] = (), + exceptions: Sequence[Any] = (), + gaps: Sequence[Any] = (), + coverage: Any | None = None, + backfill_on_regrant: bool = False, + status_retention_days: int | None = None, + destinations: Mapping[str, Any] | None = None, + account_operator: str | None = None, + version_configurations: Mapping[int, Any] | None = None, + timezone_changes: Sequence[AccountTimezoneChange] = (), + include_unresolved: bool = False, +) -> list[ExpectedSubscriptionPeriod]: + """Every period one subscription owes one account, from plain data. + + :param subscription: the ``get_principal`` readback + (``core/reporting-subscription-state.json``) as a model or mapping, or + a sequence of :class:`SubscriptionVersionWindow`. + :param buys: the confirmed buys in the account the caller can see. + A buy without ``confirmed_at`` is not covered. + :param offerings: offerings with the seller's dimension mapping. + :param account_timezone: the account's current timezone, as + ``list_accounts`` shows it. Before a timezone change the zone is that + change's ``previous_timezone``. Required when an ``account_timezone`` + offering resolves without a ``period_timezone`` and no change applies. + :param now: periods are owed once resolved, at their end; a period ending + after ``now`` is returned only with ``include_unresolved``. + :param authorizations: the account's grant history. Empty means the seller + does not advertise ``authorized_at`` and authority never lapsed. + :param exceptions: ``subscription_coverage[].excluded`` entries, or + :class:`SubscriptionException` values. + :param gaps: ``subscription_coverage[].gaps`` entries. + :param coverage: a whole ``subscription_coverage[]`` entry; its + ``excluded``, ``gaps`` and ``timezone_cutovers`` are applied too. + :param backfill_on_regrant: the seller's ``subscriptions.backfill_on_regrant``. + :param status_retention_days: the seller's status retention; required when + a grant was revoked and re-granted. + :param destinations: the caller's ``reporting_destinations`` by + ``destination_id``, needed when a version names a destination. + :param account_operator: the account's operator, for ``operator_domains``. + :param timezone_changes: account timezone changes, each a cutover at its + ``changed_at`` (``timezone_cutovers[].effective_at``). + """ + now = utc_instant(now, field="now") + windows = _windows(subscription, version_configurations) + if not windows: + return [] + subscription_id = _subscription_id(subscription, windows) + retention = None if status_retention_days is None else timedelta(days=status_retention_days) + chains = _chains(authorizations, retention) + excluded = list(exceptions) + gap_entries = list(gaps) + changes = list(timezone_changes) + if coverage is not None: + excluded.extend(_value(coverage, "excluded") or ()) + gap_entries.extend(_value(coverage, "gaps") or ()) + for entry in _value(coverage, "timezone_cutovers") or (): + offering_id = _value(entry, "offering_id") + previous = _value(entry, "previous_timezone") + changes.append( + AccountTimezoneChange( + _require_instant(_value(entry, "effective_at"), field="effective_at"), + _text(_value(entry, "timezone")), + offering_id=None if offering_id is None else _text(offering_id), + previous_timezone=None if previous is None else _text(previous), + ) + ) + changes.sort(key=lambda item: item.changed_at) + if account_timezone is not None: + require_zone(account_timezone, field="account_timezone") + for change in changes: + require_zone(change.timezone, field="timezone_cutovers.timezone") + for change in changes: + if change.previous_timezone is None: + raise ValueError( + "a timezone change needs previous_timezone: the account's current timezone " + "already shows the new zone" + ) + require_zone(change.previous_timezone, field="timezone_cutovers.previous_timezone") + changes.sort(key=lambda item: item.changed_at) + removals = subscription_exceptions(excluded, gap_entries) + ids = [item.offering_id for item in offerings] + if len(set(ids)) != len(ids): + raise ValueError("offering_id values must be unique") + buy_ids = [item.media_buy_id for item in buys] + if len(set(buy_ids)) != len(buy_ids): + raise ValueError("media_buy_id values must be unique") + + points: set[datetime] = set() + for window in windows: + points.add(window.accepted_at) + if window.ends_at is not None: + points.add(window.ends_at) + for start, until in window.inactive_intervals: + points.add(start) + if until is not None: + points.add(until) + for item in offerings: + if item.available_from is not None: + points.add(item.available_from) + if item.withdrawn_at is not None: + points.add(item.withdrawn_at) + points.update(change.changed_at for change in changes) + ordered = sorted(points) + selectors = { + window.subscription_version: SubscriptionSelector.from_subscription(window.configuration) + for window in windows + } + in_scope = { + window.subscription_version: subscription_covers_account( + window.configuration, account_id=account_id, account_operator=account_operator + ) + for window in windows + } + + owed: dict[tuple[int, str, str, datetime], _Owed] = {} + for buy in buys: + if buy.confirmed_at is None: + continue + regimes = _regimes( + buy, + ordered, + windows, + selectors, + in_scope, + offerings, + destinations, + account_timezone, + changes, + ) + flights_start = buy._earliest_start() + latest_stop = buy._latest_stop() + for index, regime in enumerate(regimes): + if regime is None: + continue + for resolved in regime.resolved: + calendar = resolved.calendar + handoff = regime.handoffs[resolved.offering.offering_id] + lower = max( + _first_owed_start(calendar, handoff, buy.confirmed_at), + calendar.floor(flights_start), + ) + upper = latest_stop if regime.until is None else min(regime.until, latest_stop) + if not include_unresolved: + upper = min(upper, now) + if lower >= upper: + continue + for start, end in calendar.periods(starts_from=lower, starts_before=upper): + if not include_unresolved and end > now: + break + flight_start, flight_stop = buy.flight_at(end) + # An empty flight (stopped at or before it started) + # overlaps no period. + if flight_stop <= flight_start or not ( + start < flight_stop and end > flight_start + ): + continue + if not _authorized(chains, calendar, start, backfill_on_regrant): + continue + if any( + item.removes(start, buy.media_buy_id, resolved.offering.offering_id) + for item in removals + ): + continue + key = (regime.version, resolved.offering.offering_id, calendar.zone_name, start) + entry = owed.get(key) + if entry is None: + expected_at, withheld = _expected_at( + chains, calendar, start, end, backfill_on_regrant + ) + entry = owed[key] = _Owed( + regime.version, + resolved.offering, + calendar.zone_name, + start, + end, + expected_at, + withheld, + ) + entry.media_buy_ids.add(buy.media_buy_id) + + periods = [ + ExpectedSubscriptionPeriod( + delivery_config_id=subscription_id, + delivery_config_version=item.version, + report_definition_id=item.offering.report_definition_id, + feed_purpose=item.offering.feed_purpose, + reporting_profile=item.offering.reporting_profile, + media_buy_ids=tuple(sorted(item.media_buy_ids, key=lambda value: value.encode())), + period_start=iso_z(item.start), + period_end=iso_z(item.end), + source_timezone=item.zone, + expected_at=None if item.expected_at is None else iso_z(item.expected_at), + account_id=account_id, + subscription_id=subscription_id, + offering_id=item.offering.offering_id, + withheld=item.withheld, + ) + for item in owed.values() + ] + periods.sort( + key=lambda item: ( + utc_instant(datetime.fromisoformat(item.period_start.replace("Z", "+00:00"))), + item.delivery_config_version, + item.offering_id.encode("utf-8"), + ) + ) + return periods + + +def _regimes( + buy: SubscriptionBuy, + ordered: Sequence[datetime], + windows: Sequence[SubscriptionVersionWindow], + selectors: Mapping[int, SubscriptionSelector], + in_scope: Mapping[int, bool], + offerings: Sequence[SubscriptionOffering], + destinations: Mapping[str, Any] | None, + account_timezone: str | None, + changes: Sequence[AccountTimezoneChange], +) -> list[_Regime | None]: + """Maximal stretches with one active, in-scope version and one resolution.""" + regimes: list[_Regime | None] = [] + for index, start in enumerate(ordered): + until = ordered[index + 1] if index + 1 < len(ordered) else None + window = next((item for item in windows if item.active_at(start)), None) + if window is None or not in_scope[window.subscription_version]: + if regimes and regimes[-1] is not None: + regimes.append(None) + continue + selector = selectors[window.subscription_version] + destination = window.destination + if destination is None and selector.destination_id is not None: + if destinations is None or selector.destination_id not in destinations: + raise ValueError( + f"version {window.subscription_version} names destination " + f"{selector.destination_id!r}; pass its pattern and transport" + ) + destination = destinations[selector.destination_id] + found: dict[str, SubscriptionOffering] = {} + for _package_id, offering_ids in buy.product_offering_ids(): + resolution = resolve_subscription_offerings( + selector, + offerings, + product_offering_ids=offering_ids, + destination=destination, + at=start, + ) + for item in resolution.offerings: + found[item.offering_id] = item + resolved = tuple( + _Resolved( + item, + item.calendar( + _timezone_at(start, account_timezone, changes, item.offering_id), + selector.period_timezone, + ), + ) + for item in sorted(found.values(), key=lambda value: value.offering_id.encode()) + ) + previous = regimes[-1] if regimes else None + if ( + previous is not None + and previous.version == window.subscription_version + and [item.key for item in previous.resolved] == [item.key for item in resolved] + ): + previous.until = until + continue + regime = _Regime(start, until, window.subscription_version, resolved) + predecessor = regimes[-1] if regimes else None + regime.handoffs = { + item.offering.offering_id: _cutover_end(start, predecessor, item) for item in resolved + } + regimes.append(regime) + return regimes + + +def _first_owed_start( + calendar: PeriodCalendar, handoff: datetime, confirmed_at: datetime +) -> datetime: + """The earliest start a buy's owed period may have under one regime. + + adcp#8593 (owner decision for 3.3.0-beta.2): the start bound is the first + boundary at or after the later of the version's ``accepted_at`` (or the + cutover E, the regime's handoff) and the account's ``authorized_at``, + which the authority chains apply. A buy is then owed every period that + ends after its ``confirmed_at``, so the period containing the + confirmation is owed in full. This is the one place the confirmation rule + lives. + """ + return max(calendar.ceil(handoff), calendar.floor(confirmed_at)) + + +def _cutover_end(handoff: datetime, predecessor: _Regime | None, incoming: _Resolved) -> datetime: + """E for an incoming offering at ``handoff``; the regime start with no predecessor. + + E is the end of the outgoing offering's last period that starts before T. + When the outgoing regime itself owed nothing before T (its own handoff + lies after T, as with two cutovers inside one period), that last period + belongs to an earlier regime and ends at the outgoing regime's handoff. + """ + if predecessor is None or not predecessor.resolved: + return handoff + + def outgoing_end(item: _Resolved) -> datetime: + return max( + item.calendar.ceil(handoff), + predecessor.handoffs.get(item.offering.offering_id, handoff), + ) + + for item in predecessor.resolved: + if item.offering.offering_id == incoming.offering.offering_id: + return outgoing_end(item) + return max(outgoing_end(item) for item in predecessor.resolved) + + +def _subscription_id(subscription: Any, windows: Sequence[SubscriptionVersionWindow]) -> str: + value = None + if not isinstance(subscription, Sequence) or isinstance(subscription, Mapping): + value = _value(subscription, "subscription_id") + if value is None: + configuration = windows[-1].configuration + if hasattr(configuration, "model_dump"): + configuration = configuration.model_dump(mode="json", exclude_none=True) + value = _value(configuration, "subscription_id") + if value is None: + raise ValueError("subscription_id is required") + return _text(value) + + +__all__ = [ + "AccountTimezoneChange", + "ExpectedSubscriptionPeriod", + "SubscriptionAuthorization", + "SubscriptionBuy", + "SubscriptionBuyFlight", + "SubscriptionBuyPackage", + "SubscriptionException", + "SubscriptionVersionWindow", + "expected_subscription_periods", + "subscription_covers_account", + "subscription_exceptions", + "subscription_version_windows", +] diff --git a/src/adcp/reporting/subscriptions/resolution.py b/src/adcp/reporting/subscriptions/resolution.py new file mode 100644 index 000000000..c02513c51 --- /dev/null +++ b/src/adcp/reporting/subscriptions/resolution.py @@ -0,0 +1,479 @@ +"""Resolve a reporting-subscription selector to offerings, as both sides must. + +``core/reporting-subscription.json`` (``x-adcp-validation.selection``): an +offering is a candidate when ``feed_purpose``, ``period_duration``, +``reconciliation_mode``, ``required_finality`` (in ``supported_finality``) and +a present ``alignment`` match, a present destination matches its ``method`` +(no destination selects only API-delivered offerings), and the product lists +it in ``reporting_delivery_offering_ids``. ``match`` then applies: + +* ``exact``: the offering's dimension set equals the selector's. Native + dimensions that do not map to the standard vocabulary count as distinct. +* ``finest``: the single offering with the most mapped dimensions among those + including every selector dimension. +* ``all``: every offering including every selector dimension. + +Ties under ``exact`` and ``finest`` go to the lowest ``offering_id`` in byte +order. ``match`` defaults to ``exact`` when ``dimensions`` is present and to +``finest`` when it is absent. + +Offerings publish the seller's mapping as ``standard_dimensions`` and +``unmapped_dimensions`` (adcp#8585), so +:meth:`SubscriptionOffering.from_offering` reads them from the wire and a +buyer resolves exactly what the seller resolves. +""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping, Sequence +from dataclasses import dataclass, field +from datetime import datetime +from typing import Any, Literal + +from adcp.reporting.subscriptions._clock import ( + PeriodCalendar, + parse_delivery_sla, + parse_period_duration, + require_zone, + utc_instant, +) + +#: The 3.3 standard breakdown vocabulary (``enums/reporting-dimension.json``). +#: It is extensible: a selector naming a value no offering maps matches nothing. +STANDARD_REPORTING_DIMENSIONS: frozenset[str] = frozenset( + {"package", "creative", "country", "region", "metro"} +) + +SubscriptionMatch = Literal["exact", "finest", "all"] +_ALIGNMENTS = frozenset({"utc", "account_timezone", "source_timezone", "billing_cycle"}) +#: The UTC aliases a ``utc`` schedule matches under ``period_timezone`` +#: (``core/reporting-subscription.json``: "a UTC alias (UTC or Etc/UTC)"). +UTC_ZONE_NAMES: frozenset[str] = frozenset({"UTC", "Etc/UTC"}) + + +def _value(item: Any, name: str, default: Any = None) -> Any: + if isinstance(item, Mapping): + value = item.get(name, default) + else: + value = getattr(item, name, default) + return default if value is None else value + + +def _text(value: Any) -> str: + """Enum-safe string projection of generated-model values.""" + inner = getattr(value, "value", value) + return str(inner) + + +def _byte_key(offering_id: str) -> bytes: + return offering_id.encode("utf-8") + + +@dataclass(frozen=True) +class SubscriptionOffering: + """One reporting-delivery offering, with the seller's dimension mapping. + + ``dimensions`` are the standard-vocabulary values the offering's report + definition maps onto. ``unmapped_dimensions`` are native dimensions with no + vocabulary mapping; they make an ``exact`` match fail and are ignored when + ``finest`` counts mapped dimensions. + + ``period_timezone`` is the schedule's IANA zone for ``source_timezone`` + (fixed, or the seller's account-resolved upstream zone) and fixed + ``billing_cycle`` offerings. ``period_timezone_policy`` is the + ``source_timezone`` policy: an ``account_resolved`` schedule has no + published zone, so it never matches a ``period_timezone`` override. + ``available_from`` and ``withdrawn_at`` bound when the offering is a + candidate; a change in resolution is a cutover. + """ + + offering_id: str + report_definition_id: str + reporting_profile: str + feed_purpose: str + period_duration: str + alignment: str + delivery_sla: str + supported_finality: frozenset[str] + reconciliation_mode: str + dimensions: frozenset[str] = frozenset() + unmapped_dimensions: frozenset[str] = frozenset() + period_timezone: str | None = None + period_anchor: datetime | None = None + period_anchor_policy: str | None = None + period_timezone_policy: str | None = None + method_pattern: str | None = None + method_transport: str | None = None + available_from: datetime | None = None + withdrawn_at: datetime | None = None + + def __post_init__(self) -> None: + if not self.offering_id: + raise ValueError("offering_id is required") + if self.alignment not in _ALIGNMENTS: + raise ValueError(f"unknown schedule alignment {self.alignment!r}") + # Refuse what the shared calendar cannot derive, up front rather than + # mid-evaluation: a delivery_sla with years or months, or an invalid + # duration. + parse_delivery_sla(self.delivery_sla) + parse_period_duration(self.period_duration) + object.__setattr__(self, "supported_finality", frozenset(self.supported_finality)) + object.__setattr__(self, "dimensions", frozenset(self.dimensions)) + object.__setattr__(self, "unmapped_dimensions", frozenset(self.unmapped_dimensions)) + overlap = self.dimensions & self.unmapped_dimensions + if overlap: + raise ValueError(f"dimensions are both mapped and unmapped: {sorted(overlap)}") + if self.period_timezone is not None: + require_zone(self.period_timezone, field="period_timezone") + if self.period_anchor is not None: + object.__setattr__( + self, "period_anchor", utc_instant(self.period_anchor, field="period_anchor") + ) + for name in ("available_from", "withdrawn_at"): + value = getattr(self, name) + if value is not None: + object.__setattr__(self, name, utc_instant(value, field=name)) + if ( + self.available_from is not None + and self.withdrawn_at is not None + and self.withdrawn_at <= self.available_from + ): + raise ValueError("withdrawn_at must be after available_from") + + @classmethod + def from_offering( + cls, + offering: Any, + *, + dimensions: Iterable[str] | None = None, + unmapped_dimensions: Iterable[str] | None = None, + period_timezone: str | None = None, + available_from: datetime | None = None, + withdrawn_at: datetime | None = None, + ) -> SubscriptionOffering: + """Project a ``ReportingDeliveryOffering`` (model or wire mapping). + + Dimensions come from the offering's ``standard_dimensions`` and + ``unmapped_dimensions`` (adcp#8585); an offering without + ``standard_dimensions`` is refused unless ``dimensions`` supplies the + mapping explicitly. ``period_timezone`` supplies the account-resolved zone of an + ``account_resolved`` ``source_timezone`` offering; a fixed zone on the + offering wins when both are present and they must agree. + """ + if hasattr(offering, "model_dump"): + offering = offering.model_dump(mode="json", exclude_none=True) + if not isinstance(offering, Mapping): + raise TypeError("offering must be a ReportingDeliveryOffering or a mapping") + if dimensions is None: + if "standard_dimensions" not in offering: + # adcp#8585: sellers advertising subscriptions MUST publish it, + # and matching never guesses a breakdown. + raise ValueError( + f"offering {offering.get('offering_id')!r} has no standard_dimensions" + ) + dimensions = tuple(offering["standard_dimensions"] or ()) + schedule = offering["schedule"] + fixed_zone = schedule.get("period_timezone") + if fixed_zone is not None and period_timezone is not None and fixed_zone != period_timezone: + raise ValueError("period_timezone disagrees with the offering's fixed timezone") + anchor = schedule.get("period_anchor") + method = offering.get("method") or {} + profile = offering["reporting_profile"] + return cls( + offering_id=str(offering["offering_id"]), + report_definition_id=str(offering["report_definition_id"]), + reporting_profile=str(profile["id"] if isinstance(profile, Mapping) else profile), + feed_purpose=str(offering["feed_purpose"]), + period_duration=str(schedule["period_duration"]), + alignment=str(schedule["alignment"]), + delivery_sla=str(schedule["delivery_sla"]), + supported_finality=frozenset(str(item) for item in offering["supported_finality"]), + reconciliation_mode=str(offering["reconciliation_mode"]), + dimensions=frozenset(dimensions), + unmapped_dimensions=frozenset( + offering.get("unmapped_dimensions") or () + if unmapped_dimensions is None + else unmapped_dimensions + ), + period_timezone=fixed_zone or period_timezone, + period_anchor=( + datetime.fromisoformat(str(anchor).replace("Z", "+00:00")) if anchor else None + ), + period_anchor_policy=schedule.get("period_anchor_policy"), + period_timezone_policy=schedule.get("period_timezone_policy"), + method_pattern=method.get("pattern"), + method_transport=method.get("transport"), + available_from=available_from, + withdrawn_at=withdrawn_at, + ) + + def available_at(self, instant: datetime) -> bool: + return (self.available_from is None or self.available_from <= instant) and ( + self.withdrawn_at is None or instant < self.withdrawn_at + ) + + @property + def schedulable(self) -> bool: + """Whether a subscription can derive this offering's boundaries. + + A subscription carries no billing anchor, so a ``billing_cycle`` + offering whose anchor is not fixed is never resolvable. + """ + return self.alignment != "billing_cycle" or ( + self.period_anchor is not None and self.period_timezone is not None + ) + + def can_produce(self, period_timezone: str | None) -> bool: + """Whether the offering matches a ``period_timezone`` override. + + ``core/reporting-subscription.json`` ``selector.period_timezone``: an + ``account_timezone`` offering matches and uses the override in place of + the account's zone; a ``utc`` offering matches only a UTC alias (UTC or + Etc/UTC); a ``source_timezone`` or fixed ``billing_cycle`` offering + matches only when its published ``period_timezone`` equals it; a + ``billing_cycle`` offering whose anchor the buyer configures never + matches a subscription. + """ + if not self.schedulable: + return False + if period_timezone is None: + return True + if self.alignment == "utc": + return period_timezone in UTC_ZONE_NAMES + if self.alignment == "account_timezone": + return True + if self.alignment == "source_timezone": + return ( + self.period_timezone_policy == "fixed" and self.period_timezone == period_timezone + ) + return self.period_timezone == period_timezone + + def zone(self, account_timezone: str | None, period_timezone: str | None = None) -> str: + """The IANA zone this offering's periods use for an account. + + ``period_timezone`` is a single-account subscription's override; it + applies to ``account_timezone`` schedules only (other alignments match + an override only when their published zone already equals it). + """ + if self.alignment == "utc": + return "UTC" + if self.alignment == "account_timezone": + zone = period_timezone or account_timezone + if zone is None: + raise ValueError( + f"offering {self.offering_id!r} is account_timezone aligned; " + "the account timezone is required" + ) + return zone + if self.alignment == "source_timezone": + zone = self.period_timezone + if zone is None: + raise ValueError( + f"offering {self.offering_id!r} resolves its source timezone per account; " + "pass the resolved zone as period_timezone" + ) + return zone + if self.period_timezone is None: + raise ValueError(f"offering {self.offering_id!r} has no period_timezone") + return self.period_timezone + + def calendar( + self, account_timezone: str | None, period_timezone: str | None = None + ) -> PeriodCalendar: + """This offering's normative period calendar for an account.""" + return PeriodCalendar.for_schedule( + period_duration=self.period_duration, + delivery_sla=self.delivery_sla, + zone_name=self.zone(account_timezone, period_timezone), + anchor=self.period_anchor if self.alignment == "billing_cycle" else None, + local=self.alignment != "utc", + ) + + +@dataclass(frozen=True) +class SubscriptionDestination: + """The delivery pattern and transport of a principal reporting destination.""" + + pattern: str + transport: str | None = None + + @classmethod + def coerce(cls, value: Any) -> SubscriptionDestination: + if isinstance(value, SubscriptionDestination): + return value + configuration = _value(value, "configuration") + source = configuration if configuration is not None else value + if hasattr(source, "model_dump"): + source = source.model_dump(mode="json", exclude_none=True) + root = getattr(source, "root", None) + if root is not None: + source = root.model_dump(mode="json", exclude_none=True) + pattern = _value(source, "pattern") + if pattern is None: + raise ValueError("a reporting destination needs a pattern") + transport = _value(source, "transport") + return cls( + pattern=_text(pattern), transport=None if transport is None else _text(transport) + ) + + +@dataclass(frozen=True) +class SubscriptionSelector: + """Normalized ``selector`` plus the subscription's ``reconciliation_mode``.""" + + feed_purpose: str + period_duration: str + required_finality: str + reconciliation_mode: str + match: SubscriptionMatch + dimensions: frozenset[str] | None = None + alignment: str | None = None + period_timezone: str | None = None + destination_id: str | None = None + + @classmethod + def from_subscription(cls, subscription: Any) -> SubscriptionSelector: + """Read ``selector``, ``reconciliation_mode`` and destination from a subscription.""" + if hasattr(subscription, "model_dump"): + subscription = subscription.model_dump(mode="json", exclude_none=True) + selector = _value(subscription, "selector") + if selector is None: + raise ValueError("subscription has no selector") + raw_dimensions = _value(selector, "dimensions") + dimensions = ( + None if raw_dimensions is None else frozenset(_text(item) for item in raw_dimensions) + ) + raw_match = _value(selector, "match") + match: str = ( + _text(raw_match) + if raw_match is not None + else ("exact" if dimensions is not None else "finest") + ) + if match not in {"exact", "finest", "all"}: + raise ValueError(f"unknown selector match {match!r}") + destination = _value(subscription, "destination") + alignment = _value(selector, "alignment") + period_timezone = _value(selector, "period_timezone") + return cls( + feed_purpose=_text(_value(selector, "feed_purpose")), + period_duration=_text(_value(selector, "period_duration")), + required_finality=_text(_value(selector, "required_finality")), + reconciliation_mode=_text(_value(subscription, "reconciliation_mode")), + match=match, # type: ignore[arg-type] + dimensions=dimensions, + alignment=None if alignment is None else _text(alignment), + period_timezone=None if period_timezone is None else _text(period_timezone), + destination_id=( + None if destination is None else _text(_value(destination, "destination_id")) + ), + ) + + +@dataclass(frozen=True) +class SubscriptionResolution: + """Offerings a selector resolved to for one product, in byte order.""" + + offerings: tuple[SubscriptionOffering, ...] + candidates: tuple[str, ...] = field(default=()) + + @property + def offering_ids(self) -> tuple[str, ...]: + return tuple(item.offering_id for item in self.offerings) + + +def _is_candidate( + offering: SubscriptionOffering, + selector: SubscriptionSelector, + *, + destination: SubscriptionDestination | None, +) -> bool: + if ( + offering.feed_purpose != selector.feed_purpose + or offering.period_duration != selector.period_duration + or offering.reconciliation_mode != selector.reconciliation_mode + or selector.required_finality not in offering.supported_finality + ): + return False + if selector.alignment is not None and offering.alignment != selector.alignment: + return False + if destination is None: + if offering.method_pattern is not None: + return False + elif ( + offering.method_pattern != destination.pattern + or offering.method_transport != destination.transport + ): + return False + return offering.can_produce(selector.period_timezone) + + +def resolve_subscription_offerings( + subscription: Any, + offerings: Sequence[SubscriptionOffering], + *, + product_offering_ids: Iterable[str], + destination: Any | None = None, + at: datetime | None = None, +) -> SubscriptionResolution: + """Resolve one subscription for one product, deterministically. + + ``subscription`` is a ``ReportingSubscription`` (model or mapping) or a + :class:`SubscriptionSelector`. ``destination`` is the named principal + reporting destination (model, mapping, or :class:`SubscriptionDestination`) + and is required when the subscription names one. ``at`` restricts + candidates to offerings available at that instant. + """ + selector = ( + subscription + if isinstance(subscription, SubscriptionSelector) + else SubscriptionSelector.from_subscription(subscription) + ) + if selector.destination_id is not None and destination is None: + raise ValueError( + f"subscription names destination {selector.destination_id!r}; pass its pattern" + ) + target = None if destination is None else SubscriptionDestination.coerce(destination) + listed = set(product_offering_ids) + instant = None if at is None else utc_instant(at, field="at") + candidates = sorted( + ( + offering + for offering in offerings + if offering.offering_id in listed + and (instant is None or offering.available_at(instant)) + and _is_candidate(offering, selector, destination=target) + ), + key=lambda item: _byte_key(item.offering_id), + ) + wanted = selector.dimensions + if selector.match == "exact": + matches = [ + item + for item in candidates + if wanted is None or (item.dimensions == wanted and not item.unmapped_dimensions) + ] + chosen: tuple[SubscriptionOffering, ...] = tuple(matches[:1]) + else: + including = [item for item in candidates if wanted is None or wanted <= item.dimensions] + if selector.match == "all": + chosen = tuple(including) + elif including: + most = max(len(item.dimensions) for item in including) + chosen = (next(item for item in including if len(item.dimensions) == most),) + else: + chosen = () + return SubscriptionResolution( + offerings=chosen, candidates=tuple(item.offering_id for item in candidates) + ) + + +__all__ = [ + "STANDARD_REPORTING_DIMENSIONS", + "UTC_ZONE_NAMES", + "SubscriptionDestination", + "SubscriptionMatch", + "SubscriptionOffering", + "SubscriptionResolution", + "SubscriptionSelector", + "resolve_subscription_offerings", +] diff --git a/src/adcp/reporting/subscriptions/wire.py b/src/adcp/reporting/subscriptions/wire.py new file mode 100644 index 000000000..241f04f89 --- /dev/null +++ b/src/adcp/reporting/subscriptions/wire.py @@ -0,0 +1,167 @@ +"""Version-gated wire projection for the fields adcp#8585 added after 3.3.0-beta.1. + +adcontextprotocol/adcp#8585 adds ``standard_dimensions`` and +``unmapped_dimensions`` on offerings, ``inactive_intervals`` on the +subscription readback, and ``previous_timezone`` and ``offering_id`` on +``timezone_cutovers[]`` and ``gaps[]``. The pinned 3.3.0-beta.1 schemas close +those objects, and the server validates responses against the release it +serves, so these fields reach only callers served 3.3.0-beta.2 or later (the +same gate the TypeScript SDK applies). For older callers: + +* the new fields are dropped; +* per-offering ``gaps[]`` collapse into one account-wide gap spanning them + (it removes at least the periods any offering's gap removes); +* per-offering ``timezone_cutovers[]`` for one change collapse into one entry + at the latest ``effective_at``; distinct changes stay distinct; +* ``prior_versions[].configuration`` (adcp#8588) is dropped. + +Known limitation: without ``inactive_intervals`` a beta.1 reader cannot see an +in-place reactivation, so it expects the periods of the inactive stretch. +3.3.0-beta.1 has no way to express it. + +TODO(3.3.0-beta.2): once the SDK pins a bundle containing adcp#8585 these +projections only matter for callers pinned to 3.3.0-beta.1. +""" + +from __future__ import annotations + +import re +from collections.abc import Mapping, Sequence +from datetime import datetime +from typing import Any + +_VERSION = re.compile(r"^(\d+)\.(\d+)(?:\.\d+)?(?:-([A-Za-z]+)\.?(\d+)?)?$") +_OFFERING_FIELDS = ("standard_dimensions", "unmapped_dimensions") + + +def serves_settled_subscription_fields(adcp_version: str | None) -> bool: + """Whether a response served at ``adcp_version`` may carry the adcp#8585 fields. + + True for 3.3.0-beta.2 and later prereleases, any 3.3 release candidate or + final release, and anything later. An absent or unparseable version is + treated as 3.3.0-beta.1, the pinned default. + """ + if not adcp_version: + return False + match = _VERSION.match(adcp_version.strip()) + if match is None: + return False + major, minor, tag, number = match.groups() + release = (int(major), int(minor)) + if release != (3, 3): + return release > (3, 3) + if tag is None: + return True + tag = tag.lower() + if tag == "beta": + return int(number or 0) >= 2 + return tag == "rc" + + +def project_offering(offering: Mapping[str, Any], adcp_version: str | None) -> dict[str, Any]: + """A reporting-delivery offering as served at ``adcp_version``.""" + payload = dict(offering) + if not serves_settled_subscription_fields(adcp_version): + for name in _OFFERING_FIELDS: + payload.pop(name, None) + return payload + + +def project_reporting_delivery( + block: Mapping[str, Any], adcp_version: str | None +) -> dict[str, Any]: + """A ``reporting_delivery`` capability block as served at ``adcp_version``.""" + payload = dict(block) + if "offerings" in payload: + payload["offerings"] = [ + project_offering(item, adcp_version) for item in payload["offerings"] + ] + return payload + + +def project_subscription_state( + state: Mapping[str, Any], adcp_version: str | None +) -> dict[str, Any]: + """A ``core/reporting-subscription-state.json`` readback as served.""" + payload = dict(state) + if not serves_settled_subscription_fields(adcp_version): + payload.pop("inactive_intervals", None) + if payload.get("prior_versions"): + payload["prior_versions"] = [ + {key: value for key, value in item.items() if key != "configuration"} + for item in payload["prior_versions"] + ] + return payload + + +def project_subscription_coverage( + coverage: Mapping[str, Any], adcp_version: str | None +) -> dict[str, Any]: + """A ``subscription_coverage[]`` entry as served at ``adcp_version``.""" + payload = dict(coverage) + if serves_settled_subscription_fields(adcp_version): + return payload + if payload.get("gaps"): + payload["gaps"] = _account_wide_gaps(payload["gaps"]) + if payload.get("timezone_cutovers"): + payload["timezone_cutovers"] = _account_wide_cutovers(payload["timezone_cutovers"]) + return payload + + +def _instant(value: Any) -> datetime: + if isinstance(value, datetime): + return value + return datetime.fromisoformat(str(value).replace("Z", "+00:00")) + + +def _account_wide_gaps(gaps: Sequence[Mapping[str, Any]]) -> list[dict[str, Any]]: + """Drop ``offering_id``; merge overlapping per-offering gaps into their span.""" + plain = [dict(gap) for gap in gaps if "offering_id" not in gap] + scoped = sorted( + ( + {k: v for k, v in gap.items() if k != "offering_id"} + for gap in gaps + if "offering_id" in gap + ), + key=lambda item: _instant(item["from"]), + ) + merged: list[dict[str, Any]] = [] + for entry in scoped: + last = merged[-1] if merged else None + if last is not None and _instant(entry["from"]) <= _instant(last["until"]): + if _instant(entry["until"]) > _instant(last["until"]): + last["until"] = entry["until"] + continue + merged.append(entry) + result = plain + [entry for entry in merged if entry not in plain] + return sorted(result, key=lambda item: _instant(item["from"])) + + +def _account_wide_cutovers(cutovers: Sequence[Mapping[str, Any]]) -> list[dict[str, Any]]: + """One entry per change, at the latest of its per-offering ``effective_at``. + + The per-offering entries of one change share ``timezone`` and + ``previous_timezone`` and sit next to each other in time order; a later + change, even back to an earlier zone, starts a new group. + """ + ordered = sorted(cutovers, key=lambda item: _instant(item["effective_at"])) + result: list[dict[str, Any]] = [] + previous_key: tuple[Any, Any] | None = None + for cutover in ordered: + key = (cutover.get("timezone"), cutover.get("previous_timezone")) + entry = {"effective_at": cutover["effective_at"], "timezone": cutover["timezone"]} + if result and key == previous_key and "offering_id" in cutover: + result[-1] = entry # a later effective_at of the same change + else: + result.append(entry) + previous_key = key + return result + + +__all__ = [ + "project_offering", + "project_reporting_delivery", + "project_subscription_coverage", + "project_subscription_state", + "serves_settled_subscription_fields", +] diff --git a/src/adcp/types/_forward_compat.py b/src/adcp/types/_forward_compat.py index a31b2c90b..37942a763 100644 --- a/src/adcp/types/_forward_compat.py +++ b/src/adcp/types/_forward_compat.py @@ -535,5 +535,10 @@ def _apply_forward_compat() -> None: ) BundledCapabilitiesPortfolio.model_rebuild(force=True) + # Reporting-subscription fields settled after 3.3.0-beta.1 (adcp#8585). + from adcp.types._reporting_subscription_preview import apply_reporting_subscription_preview + + apply_reporting_subscription_preview() + _apply_forward_compat() diff --git a/src/adcp/types/_reporting_subscription_preview.py b/src/adcp/types/_reporting_subscription_preview.py new file mode 100644 index 000000000..87ed25da8 --- /dev/null +++ b/src/adcp/types/_reporting_subscription_preview.py @@ -0,0 +1,167 @@ +"""AdCP 3.3 reporting-subscription fields settled after 3.3.0-beta.1 (adcp#8585, adcp#8588). + +adcontextprotocol/adcp#8585 (merged to the spec's main, not yet in a signed +bundle) adds these experimental fields: + +* ``core/reporting-delivery-offering.json``: ``standard_dimensions`` and + ``unmapped_dimensions``, so a buyer resolves a selector's ``match`` from the + wire; +* ``core/reporting-subscription-state.json``: ``inactive_intervals[]``, so a + reactivation in place can be recomputed after local-state loss; +* ``core/reporting-subscription-coverage.json``: ``previous_timezone`` and + ``offering_id`` on ``timezone_cutovers[]``, and ``offering_id`` on ``gaps[]``; +* ``prior_versions[].configuration`` on the readback (adcp#8588, pending); +* the extensible ``reporting-dimension`` vocabulary on selectors, offerings and + the capability block, which the generator closed. + +The pinned 3.3.0-beta.1 models close those objects +(``additionalProperties: false``), so the fields are added to the generated +classes here, the same way :mod:`adcp.types._forward_compat` carries other +forward-looking surfaces. TODO(3.3.0-beta.2): drop this module when the SDK +adopts a bundle that contains adcp#8585 and the fields are generated. +""" + +from __future__ import annotations + +from typing import Annotated, Any, cast + +from pydantic import BaseModel, ConfigDict, Field +from pydantic.fields import FieldInfo + +from adcp.types.base import AdCPBaseModel, NoExplicitNull, WireDatetime +from adcp.types.domains.bundled.protocol import ( + get_adcp_capabilities_response as _bundled_capabilities, +) +from adcp.types.domains.core import reporting_delivery_capabilities as _capabilities +from adcp.types.domains.core import reporting_delivery_offering as _offering +from adcp.types.domains.core import reporting_subscription as _subscription +from adcp.types.domains.core import reporting_subscription_coverage as _coverage +from adcp.types.domains.core import reporting_subscription_state as _state + +#: One value of the extensible ``enums/reporting-dimension.json`` vocabulary. +#: Receivers MUST accept unknown values (``x-extensible``); a selector naming a +#: value no offering maps simply matches nothing. +ReportingDimensionToken = Annotated[str, Field(pattern=r"^[a-z][a-z0-9_]*$", max_length=64)] + + +class ReportingSubscriptionInactiveInterval(AdCPBaseModel): + """One stretch during which a subscription was inactive (adcp#8585).""" + + model_config = ConfigDict(extra="forbid") + deactivated_at: Annotated[WireDatetime, Field(description="When active false was applied.")] + reactivated_at: Annotated[ + WireDatetime | None, + Field( + description=( + "When active true was applied again. Absent for the current interval " + "while the subscription is inactive." + ) + ), + NoExplicitNull, + ] = None + + +def _add_field(model: type[BaseModel], name: str, annotation: Any, field: FieldInfo) -> None: + if name in model.model_fields: + return # the generated model already declares it: this preview is stale + field.annotation = annotation + model.model_fields[name] = field + model.__annotations__[name] = annotation + model.model_rebuild(force=True) + + +def _replace_field(model: type[BaseModel], name: str, annotation: Any, field: FieldInfo) -> None: + field.annotation = annotation + model.model_fields[name] = field + model.__annotations__[name] = annotation + model.model_rebuild(force=True) + + +def _optional(annotation: Any, **kwargs: Any) -> tuple[Any, FieldInfo]: + wrapped = Annotated[annotation | None, Field(**kwargs), NoExplicitNull] + return wrapped, FieldInfo.from_annotated_attribute(cast(Any, wrapped), None) + + +def apply_reporting_subscription_preview() -> None: + """Add the adcp#8585 fields to the pinned 3.3.0-beta.1 models (idempotent).""" + dimensions_doc = ( + "Experimental (media_buy.reporting_subscriptions). The offering's breakdown " + "mapped onto the standard reporting dimension vocabulary." + ) + unmapped_doc = ( + "Experimental (media_buy.reporting_subscriptions). Native report-definition " + "dimensions with no standard mapping." + ) + for offering_model in (_offering.ReportingDeliveryOffering, _bundled_capabilities.Offering): + _add_field( + offering_model, + "standard_dimensions", + *_optional(list[ReportingDimensionToken], max_length=16, description=dimensions_doc), + ) + _add_field( + offering_model, + "unmapped_dimensions", + *_optional(list[str], max_length=16, description=unmapped_doc), + ) + # The generated models close the extensible dimension vocabulary. + _replace_field( + _subscription.Selector, + "dimensions", + *_optional( + list[ReportingDimensionToken], + min_length=1, + max_length=16, + description="Breakdown in the standard (extensible) vocabulary.", + ), + ) + for subscriptions_model in (_capabilities.Subscriptions, _bundled_capabilities.Subscriptions): + _replace_field( + subscriptions_model, + "dimensions", + *_optional( + list[ReportingDimensionToken], + min_length=1, + description="Standard dimensions this seller maps its report definitions onto.", + ), + ) + # adcp#8588: each prior version carries the configuration it had. + _add_field( + _state.PriorVersion, + "configuration", + *_optional( + _subscription.ReportingSubscription, + description="The configuration this version had.", + ), + ) + _add_field( + _state.ReportingSubscriptionState, + "inactive_intervals", + *_optional( + list[ReportingSubscriptionInactiveInterval], + max_length=64, + description="Intervals during which the subscription was inactive, oldest first.", + ), + ) + _add_field( + _coverage.TimezoneCutover, + "previous_timezone", + *_optional(str, min_length=1, max_length=255, description="The zone before the change."), + ) + for model in (_coverage.TimezoneCutover, _coverage.Gap): + _add_field( + model, + "offering_id", + *_optional( + str, + min_length=1, + max_length=255, + description="The offering this entry applies to; absent for every offering.", + ), + ) + + +__all__ = [ + "ReportingDimensionToken", + "ReportingSubscriptionInactiveInterval", + "apply_reporting_subscription_preview", +] diff --git a/tests/fixtures/reporting_subscriptions/coverage-cases.json b/tests/fixtures/reporting_subscriptions/coverage-cases.json new file mode 100644 index 000000000..34e0a2eab --- /dev/null +++ b/tests/fixtures/reporting_subscriptions/coverage-cases.json @@ -0,0 +1,13632 @@ +{ + "$source": "adcontextprotocol/adcp-client@f8b48c0c8594944dd367b774585f685a47380d57:test/fixtures/reporting-subscriptions/coverage-cases.json (PR #3505)", + "$comment": "Cross-SDK parity table for expectedSubscriptionPeriods (AdCP 3.3 media_buy.reporting_subscriptions), conforming to adcontextprotocol/adcp#8585, #8588 (sub-daily local periods) and #8593 (the start bound is the later of accepted_at and authorized_at; a buy is owed every period ending after its confirmed_at). Each case gives expected_obligations (the rows grouped by version, offering and period, with byte-sorted media_buy_ids) and, where the input needs data no AdCP response carries, non_wire_inputs. Each case gives the input and the exact owed periods (or expected_error). Compare instants, not strings: the TypeScript SDK emits toISOString() form. Expected rows are hand-derived from the spec rule. subscription_id, account_id and report_definition_id are omitted from expected rows; they are input.subscription_id, input.account.account_id and the offering's report_definition_id. Rows are ordered by period_start, subscription_version, offering_id and media_buy_id (byte order).", + "cases": [ + { + "name": "baseline", + "rule": "adcp#8593: the start bound is the first boundary at or after max(accepted_at 11-01T12, authorized_at 10-01) = 11-02; owed are the periods from it that end after confirmed_at 11-02T15 and overlap the flight (11-03 on), through the period containing the stop time.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "mid-period confirmed_at", + "rule": "adcp#8593: confirmed_at 11-03T10 falls inside the first flight day. That period ends after confirmed_at, so it is owed in full; confirmed_at no longer moves the start to the next boundary.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-03T10:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "confirmed long before the flight", + "rule": "A buy confirmed before accepted_at with a later start owes nothing before start_time.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-10-20T00:00:00Z", + "start_time": "2026-11-10T00:00:00Z", + "end_time": "2026-11-12T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-10T00:00:00Z", + "period_end": "2026-11-11T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-11T00:00:00Z", + "period_end": "2026-11-12T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-10T00:00:00Z", + "period_end": "2026-11-11T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-11T00:00:00Z", + "period_end": "2026-11-12T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "stop time mid-period is owed in full", + "rule": "end_time 11-05T12 is inside the 11-05 period, which is owed in full: periods are never clipped.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-05T12:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "cancellation mid-flight", + "rule": "canceled_at 11-05T06 is the stop time (earlier than end_time 11-10); its period is owed and none after.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-10T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ], + "canceled_at": "2026-11-05T06:00:00Z" + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "early completion on a boundary", + "rule": "completed_at 11-04T00 stops the flight on a boundary: the 11-04 period does not overlap [start, stop).", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-10T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ], + "completed_at": "2026-11-04T00:00:00Z" + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "flight extension adds only unresolved periods", + "rule": "end_time moved 11-06 -> 11-08 at 11-07T12. The 11-06 period resolved at 11-07T00 under the old end_time and is never owed retroactively; 11-07 resolves after the extension.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-08T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ], + "prior_flights": [ + { + "replaced_at": "2026-11-07T12:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z" + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-07T00:00:00Z", + "period_end": "2026-11-08T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-07T00:00:00Z", + "period_end": "2026-11-08T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ], + "non_wire_inputs": [ + "buys[].prior_flights" + ] + }, + { + "name": "reinstatement after cancellation", + "rule": "Canceled at 11-04T12, reinstated at 11-06T12. 11-05 resolved while canceled and stays unowed; 11-06 onward resolve after reinstatement.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-08T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ], + "prior_flights": [ + { + "replaced_at": "2026-11-06T12:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-08T00:00:00Z", + "canceled_at": "2026-11-04T12:00:00Z" + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-07T00:00:00Z", + "period_end": "2026-11-08T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-07T00:00:00Z", + "period_end": "2026-11-08T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ], + "non_wire_inputs": [ + "buys[].prior_flights" + ] + }, + { + "name": "only resolved periods are returned", + "rule": "With now at 11-05T12, the 11-05 period has not ended.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-11-05T12:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "unconfirmed buy", + "rule": "A buy with confirmed_at null is not covered.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": null, + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [], + "expected_obligations": [] + }, + { + "name": "late authorization (authorized_at refinement)", + "rule": "authorized_at 11-04T10 is the latest of the three start terms: coverage starts at 11-05.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-11-04T10:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-07T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "new subscription version, aligned schedules", + "rule": "v2 accepted 11-04T12. v1 owes periods starting before T in full (11-03, 11-04); E = 11-05 and v2 owes from there. Contiguous, nothing twice.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + }, + "superseded_at": "2026-11-04T12:00:00Z" + }, + { + "subscription_version": 2, + "accepted_at": "2026-11-04T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 2, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only", + "exclude_account_ids": [ + "acct_other" + ] + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-07T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 2, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + }, + { + "subscription_version": 2, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 2, + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 2, + "offering_id": "off_a_pkg", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "new subscription version, misaligned boundaries", + "rule": "v1 resolves a UTC day offering, v2 (alignment account_timezone) a New York day offering. v1 owes UTC days starting before 11-04T12 (E = 11-05T00Z); v2 owes New York days starting at or after E, the first being 11-05T05Z. 11-05T00Z-05Z is owed under neither.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + }, + "superseded_at": "2026-11-04T12:00:00Z" + }, + { + "subscription_version": 2, + "accepted_at": "2026-11-04T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 2, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ], + "alignment": "account_timezone" + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-07T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg", + "off_e_pkg_acct_tz" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + }, + { + "offering_id": "off_e_pkg_acct_tz", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_e_pkg_acct_tz", + "report_definition_uri": "https://seller.example/report-definitions/off_e_pkg_acct_tz.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "account_timezone", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 2, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-05T05:00:00Z", + "period_end": "2026-11-06T05:00:00Z" + }, + { + "subscription_version": 2, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-06T05:00:00Z", + "period_end": "2026-11-07T05:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 2, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-05T05:00:00Z", + "period_end": "2026-11-06T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 2, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-06T05:00:00Z", + "period_end": "2026-11-07T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "deactivation", + "rule": "deactivated_at 11-04T12: the 11-04 period started before it and stays owed in full; nothing later.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": false, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + }, + "deactivated_at": "2026-11-04T12:00:00Z" + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-08T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "deactivation then reactivation in place", + "rule": "Readback inactive_intervals [11-04T12, 11-06T12): a cutover out at 11-04T12 and a fresh start at the first boundary at or after 11-06T12 (11-07).", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-09T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z", + "inactive_intervals": [ + { + "deactivated_at": "2026-11-04T12:00:00Z", + "reactivated_at": "2026-11-06T12:00:00Z" + } + ] + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-07T00:00:00Z", + "period_end": "2026-11-08T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-08T00:00:00Z", + "period_end": "2026-11-09T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-07T00:00:00Z", + "period_end": "2026-11-08T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-08T00:00:00Z", + "period_end": "2026-11-09T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "account new in a later version", + "rule": "v1 names another account; v2 (all accounts, accepted 11-04T12) brings acct_1 in under the ordinary start rule: 11-05.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "account_ids": [ + "acct_other" + ] + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + }, + "superseded_at": "2026-11-04T12:00:00Z" + }, + { + "subscription_version": 2, + "accepted_at": "2026-11-04T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 2, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-07T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 2, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + }, + { + "subscription_version": 2, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 2, + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 2, + "offering_id": "off_a_pkg", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "excluded_by_buyer scope", + "rule": "An all-accounts subscription that lists the account in exclude_account_ids covers nothing for it.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only", + "exclude_account_ids": [ + "acct_1" + ] + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [], + "expected_obligations": [] + }, + { + "name": "operator_domains scope", + "rule": "operator_domains keeps only accounts whose operator is listed.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only", + "operator_domains": [ + "agency.example" + ] + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ], + "operator": "other-agency.example" + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [], + "expected_obligations": [] + }, + { + "name": "revocation", + "rule": "unauthorized_at 11-05T06: periods starting at or after the last boundary at or before it (11-05) are not owed. 11-03 and 11-04 ended before it and stay owed (withheld until re-grant if undelivered).", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z", + "unauthorized_at": "2026-11-05T06:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-08T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "re-grant without backfill", + "rule": "Re-granted 11-06T18 within status_retention_days: the gap [11-05, 11-07) is not owed; coverage resumes at 11-07 and the earlier periods stay owed.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z", + "unauthorized_at": "2026-11-05T06:00:00Z" + }, + { + "authorized_at": "2026-11-06T18:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-08T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z", + "status_retention_days": 30 + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-07T00:00:00Z", + "period_end": "2026-11-08T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-07T00:00:00Z", + "period_end": "2026-11-08T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "re-grant with backfill_on_regrant", + "rule": "With backfill_on_regrant no gap is recorded and every period in the revoked interval stays owed.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z", + "unauthorized_at": "2026-11-05T06:00:00Z" + }, + { + "authorized_at": "2026-11-06T18:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-08T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z", + "status_retention_days": 30, + "backfill_on_regrant": true + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-07T00:00:00Z", + "period_end": "2026-11-08T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-07T00:00:00Z", + "period_end": "2026-11-08T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "re-grant after the unauthorized entry expired", + "rule": "status_retention_days 1: the entry expired at 11-06T06, so the 11-06T18 re-grant is a fresh start from authorized_at (11-07) and backfill does not apply.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z", + "unauthorized_at": "2026-11-05T06:00:00Z" + }, + { + "authorized_at": "2026-11-06T18:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-08T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z", + "status_retention_days": 1, + "backfill_on_regrant": true + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-07T00:00:00Z", + "period_end": "2026-11-08T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-07T00:00:00Z", + "period_end": "2026-11-08T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "seller gaps also remove periods", + "rule": "A gaps[] entry the seller published removes the periods starting in [from, until), like an exception.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z", + "coverage": { + "gaps": [ + { + "from": "2026-11-04T00:00:00Z", + "until": "2026-11-05T00:00:00Z", + "reason": "unauthorized" + } + ] + } + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "exceptions only remove periods", + "rule": "Account-level capacity [11-04, 11-06) removes 11-04 and 11-05 for every buy; a buy-level offering_unsupported removes mb_2 from 11-03; a package_ids entry removes packages, not periods; an exception before coverage adds nothing.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-07T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + }, + { + "media_buy_id": "mb_2", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-07T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z", + "coverage": { + "excluded": [ + { + "reason": "capacity", + "from": "2026-11-04T00:00:00Z", + "until": "2026-11-06T00:00:00Z" + }, + { + "reason": "offering_unsupported", + "media_buy_id": "mb_2", + "from": "2026-11-03T00:00:00Z" + }, + { + "reason": "offering_unsupported", + "media_buy_id": "mb_1", + "package_ids": [ + "pkg_9" + ], + "from": "2026-11-03T00:00:00Z" + }, + { + "reason": "excluded_by_buyer", + "from": "2026-10-01T00:00:00Z", + "until": "2026-10-05T00:00:00Z" + } + ] + } + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "max_accounts capacity exception", + "rule": "Beyond subscriptions.max_accounts the seller records an open account-level capacity exception; nothing is owed from its from.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z", + "coverage": { + "excluded": [ + { + "reason": "capacity", + "from": "2026-11-04T00:00:00Z" + } + ] + } + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "precedence over an account-level configuration", + "rule": "The caller's own sync_accounts configuration takes the account from 11-04 to 11-05 (overridden), after which replace_account_configs hands it back.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z", + "coverage": { + "excluded": [ + { + "reason": "overridden", + "from": "2026-11-04T00:00:00Z", + "until": "2026-11-05T00:00:00Z", + "overridden_by": { + "delivery_config_id": "acme-analytics" + } + } + ] + } + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "match exact (default with dimensions)", + "rule": "exact [package] picks the offering whose dimension set equals it; package x device (unmapped device) is distinct.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg", + "off_b_pkg_creative", + "off_c_pkg_country", + "off_d_pkg_device" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + }, + { + "offering_id": "off_b_pkg_creative", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_b_pkg_creative", + "report_definition_uri": "https://seller.example/report-definitions/off_b_pkg_creative.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package", + "creative" + ] + }, + { + "offering_id": "off_c_pkg_country", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_c_pkg_country", + "report_definition_uri": "https://seller.example/report-definitions/off_c_pkg_country.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package", + "country" + ] + }, + { + "offering_id": "off_d_pkg_device", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_d_pkg_device", + "report_definition_uri": "https://seller.example/report-definitions/off_d_pkg_device.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ], + "unmapped_dimensions": [ + "device" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "match exact with no equal offering", + "rule": "exact [package] against package x creative, package x country and package x device (unmapped) resolves nothing: offering_unsupported, no periods.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_b_pkg_creative", + "off_c_pkg_country", + "off_d_pkg_device" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + }, + { + "offering_id": "off_b_pkg_creative", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_b_pkg_creative", + "report_definition_uri": "https://seller.example/report-definitions/off_b_pkg_creative.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package", + "creative" + ] + }, + { + "offering_id": "off_c_pkg_country", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_c_pkg_country", + "report_definition_uri": "https://seller.example/report-definitions/off_c_pkg_country.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package", + "country" + ] + }, + { + "offering_id": "off_d_pkg_device", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_d_pkg_device", + "report_definition_uri": "https://seller.example/report-definitions/off_d_pkg_device.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ], + "unmapped_dimensions": [ + "device" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [], + "expected_obligations": [] + }, + { + "name": "match exact on two dimensions", + "rule": "exact [package, country] picks package x country.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package", + "country" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg", + "off_b_pkg_creative", + "off_c_pkg_country", + "off_d_pkg_device" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + }, + { + "offering_id": "off_b_pkg_creative", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_b_pkg_creative", + "report_definition_uri": "https://seller.example/report-definitions/off_b_pkg_creative.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package", + "creative" + ] + }, + { + "offering_id": "off_c_pkg_country", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_c_pkg_country", + "report_definition_uri": "https://seller.example/report-definitions/off_c_pkg_country.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package", + "country" + ] + }, + { + "offering_id": "off_d_pkg_device", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_d_pkg_device", + "report_definition_uri": "https://seller.example/report-definitions/off_d_pkg_device.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ], + "unmapped_dimensions": [ + "device" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_c_pkg_country", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_c_pkg_country", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_c_pkg_country", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_c_pkg_country", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_c_pkg_country", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_c_pkg_country", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "match finest", + "rule": "finest [package] picks the most mapped dimensions among offerings including package; package x creative and package x country tie at two, so the lowest offering_id in byte order wins.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ], + "match": "finest" + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg", + "off_b_pkg_creative", + "off_c_pkg_country", + "off_d_pkg_device" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + }, + { + "offering_id": "off_b_pkg_creative", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_b_pkg_creative", + "report_definition_uri": "https://seller.example/report-definitions/off_b_pkg_creative.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package", + "creative" + ] + }, + { + "offering_id": "off_c_pkg_country", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_c_pkg_country", + "report_definition_uri": "https://seller.example/report-definitions/off_c_pkg_country.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package", + "country" + ] + }, + { + "offering_id": "off_d_pkg_device", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_d_pkg_device", + "report_definition_uri": "https://seller.example/report-definitions/off_d_pkg_device.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ], + "unmapped_dimensions": [ + "device" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "match defaults to finest without dimensions", + "rule": "No dimensions: match defaults to finest over every candidate.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official" + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg", + "off_b_pkg_creative", + "off_c_pkg_country", + "off_d_pkg_device" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + }, + { + "offering_id": "off_b_pkg_creative", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_b_pkg_creative", + "report_definition_uri": "https://seller.example/report-definitions/off_b_pkg_creative.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package", + "creative" + ] + }, + { + "offering_id": "off_c_pkg_country", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_c_pkg_country", + "report_definition_uri": "https://seller.example/report-definitions/off_c_pkg_country.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package", + "country" + ] + }, + { + "offering_id": "off_d_pkg_device", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_d_pkg_device", + "report_definition_uri": "https://seller.example/report-definitions/off_d_pkg_device.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ], + "unmapped_dimensions": [ + "device" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "match all", + "rule": "all [package] picks every offering including package, each owed its own periods (package x device included: it includes package).", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ], + "match": "all" + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg", + "off_b_pkg_creative", + "off_c_pkg_country", + "off_d_pkg_device" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + }, + { + "offering_id": "off_b_pkg_creative", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_b_pkg_creative", + "report_definition_uri": "https://seller.example/report-definitions/off_b_pkg_creative.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package", + "creative" + ] + }, + { + "offering_id": "off_c_pkg_country", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_c_pkg_country", + "report_definition_uri": "https://seller.example/report-definitions/off_c_pkg_country.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package", + "country" + ] + }, + { + "offering_id": "off_d_pkg_device", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_d_pkg_device", + "report_definition_uri": "https://seller.example/report-definitions/off_d_pkg_device.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ], + "unmapped_dimensions": [ + "device" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_c_pkg_country", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_d_pkg_device", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_c_pkg_country", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_d_pkg_device", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_c_pkg_country", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_d_pkg_device", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_c_pkg_country", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_d_pkg_device", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_c_pkg_country", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_d_pkg_device", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_c_pkg_country", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_d_pkg_device", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "product eligibility gates candidates", + "rule": "Only offerings in the product's reporting_delivery_offering_ids are candidates; a package with none leaves the buy covered by its other packages.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + }, + { + "package_id": "pkg_2", + "reporting_delivery_offering_ids": [] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + }, + { + "offering_id": "off_b_pkg_creative", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_b_pkg_creative", + "report_definition_uri": "https://seller.example/report-definitions/off_b_pkg_creative.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package", + "creative" + ] + }, + { + "offering_id": "off_c_pkg_country", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_c_pkg_country", + "report_definition_uri": "https://seller.example/report-definitions/off_c_pkg_country.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package", + "country" + ] + }, + { + "offering_id": "off_d_pkg_device", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_d_pkg_device", + "report_definition_uri": "https://seller.example/report-definitions/off_d_pkg_device.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ], + "unmapped_dimensions": [ + "device" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "finality, purpose and duration must match", + "rule": "An offering advertising only snapshot finality, another feed_purpose, or another period_duration is never a candidate.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg", + "off_b_pkg", + "off_c_pkg", + "off_d_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "snapshot" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + }, + { + "offering_id": "off_b_pkg", + "feed_purpose": "pacing", + "report_definition_id": "rd_off_b_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_b_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + }, + { + "offering_id": "off_c_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_c_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_c_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "PT1H", + "alignment": "utc", + "delivery_sla": "PT1H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + }, + { + "offering_id": "off_d_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_d_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_d_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_d_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_d_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_d_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_d_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_d_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_d_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "resolution change is a cutover", + "rule": "finest [package]: package x creative becomes available at 11-04T12, changing the resolution. The old offering owes periods starting before it (11-03, 11-04); the new one owes from E = 11-05.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ], + "match": "finest" + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-07T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg", + "off_b_pkg_creative" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + }, + { + "offering_id": "off_b_pkg_creative", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_b_pkg_creative", + "report_definition_uri": "https://seller.example/report-definitions/off_b_pkg_creative.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package", + "creative" + ], + "available_from": "2026-11-04T12:00:00Z" + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ], + "non_wire_inputs": [ + "offerings[].available_from/withdrawn_at" + ] + }, + { + "name": "account timezone", + "rule": "An account_timezone offering uses New York midnights (05:00Z in November). accepted_at 11-01T12Z: first boundary 11-02T05Z. The 11-02T05Z day contains confirmed_at 11-02T15Z and overlaps the flight from 11-03T00Z, so it is owed (adcp#8593).", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_e_pkg_acct_tz" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_e_pkg_acct_tz", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_e_pkg_acct_tz", + "report_definition_uri": "https://seller.example/report-definitions/off_e_pkg_acct_tz.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "account_timezone", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-02T05:00:00Z", + "period_end": "2026-11-03T05:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-03T05:00:00Z", + "period_end": "2026-11-04T05:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-04T05:00:00Z", + "period_end": "2026-11-05T05:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-05T05:00:00Z", + "period_end": "2026-11-06T05:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-02T05:00:00Z", + "period_end": "2026-11-03T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-03T05:00:00Z", + "period_end": "2026-11-04T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-04T05:00:00Z", + "period_end": "2026-11-05T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-05T05:00:00Z", + "period_end": "2026-11-06T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "account timezone across a DST change", + "rule": "New York falls back on 2026-11-01: the 11-01 local day is 25 hours, 04:00Z to 05:00Z the next day.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-10-30T00:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-10-30T00:00:00Z", + "start_time": "2026-10-31T12:00:00Z", + "end_time": "2026-11-02T12:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_e_pkg_acct_tz" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_e_pkg_acct_tz", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_e_pkg_acct_tz", + "report_definition_uri": "https://seller.example/report-definitions/off_e_pkg_acct_tz.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "account_timezone", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-10-31T04:00:00Z", + "period_end": "2026-11-01T04:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-01T04:00:00Z", + "period_end": "2026-11-02T05:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-02T05:00:00Z", + "period_end": "2026-11-03T05:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-10-31T04:00:00Z", + "period_end": "2026-11-01T04:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-01T04:00:00Z", + "period_end": "2026-11-02T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-02T05:00:00Z", + "period_end": "2026-11-03T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "account timezone change is a cutover", + "rule": "The account moves to Los Angeles (account.timezone is the current zone); timezone_cutovers.effective_at 11-05T05Z is E and previous_timezone New York governs before it. New York days starting before E are owed, then Los Angeles days from the first boundary at or after it (11-05T08Z). 05Z-08Z is owed under neither.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/Los_Angeles", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T05:00:00Z", + "end_time": "2026-11-08T08:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_e_pkg_acct_tz" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_e_pkg_acct_tz", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_e_pkg_acct_tz", + "report_definition_uri": "https://seller.example/report-definitions/off_e_pkg_acct_tz.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "account_timezone", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z", + "coverage": { + "timezone_cutovers": [ + { + "effective_at": "2026-11-05T05:00:00Z", + "timezone": "America/Los_Angeles", + "previous_timezone": "America/New_York" + } + ] + } + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-03T05:00:00Z", + "period_end": "2026-11-04T05:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-04T05:00:00Z", + "period_end": "2026-11-05T05:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-05T08:00:00Z", + "period_end": "2026-11-06T08:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-06T08:00:00Z", + "period_end": "2026-11-07T08:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-07T08:00:00Z", + "period_end": "2026-11-08T08:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-03T05:00:00Z", + "period_end": "2026-11-04T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-04T05:00:00Z", + "period_end": "2026-11-05T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-05T08:00:00Z", + "period_end": "2026-11-06T08:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-06T08:00:00Z", + "period_end": "2026-11-07T08:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-07T08:00:00Z", + "period_end": "2026-11-08T08:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "period_timezone override", + "rule": "A single-account subscription with period_timezone Asia/Tokyo uses Tokyo midnights (15:00Z). confirmed_at 11-02T15Z is itself a boundary, so the period ending at it is not owed and the one starting at it is.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "account_ids": [ + "acct_1" + ] + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ], + "alignment": "account_timezone", + "period_timezone": "Asia/Tokyo" + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-05T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_e_pkg_acct_tz" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_e_pkg_acct_tz", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_e_pkg_acct_tz", + "report_definition_uri": "https://seller.example/report-definitions/off_e_pkg_acct_tz.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "account_timezone", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-02T15:00:00Z", + "period_end": "2026-11-03T15:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-03T15:00:00Z", + "period_end": "2026-11-04T15:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-04T15:00:00Z", + "period_end": "2026-11-05T15:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-02T15:00:00Z", + "period_end": "2026-11-03T15:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-03T15:00:00Z", + "period_end": "2026-11-04T15:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-04T15:00:00Z", + "period_end": "2026-11-05T15:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "hourly periods are never clipped", + "rule": "Flight 10:30-13:15, confirmed 10:20: the 10:00 period ends after confirmed_at and overlaps the flight, so it is owed in full (adcp#8593), and the 13:00 period containing the stop is owed in full.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "pacing", + "period_duration": "PT1H", + "required_finality": "snapshot", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-03T10:20:00Z", + "start_time": "2026-11-03T10:30:00Z", + "end_time": "2026-11-03T13:15:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_h_pkg_hourly" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_h_pkg_hourly", + "feed_purpose": "pacing", + "report_definition_id": "rd_off_h_pkg_hourly", + "report_definition_uri": "https://seller.example/report-definitions/off_h_pkg_hourly.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "PT1H", + "alignment": "utc", + "delivery_sla": "PT1H" + }, + "supported_finality": [ + "snapshot" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_h_pkg_hourly", + "period_start": "2026-11-03T10:00:00Z", + "period_end": "2026-11-03T11:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_h_pkg_hourly", + "period_start": "2026-11-03T11:00:00Z", + "period_end": "2026-11-03T12:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_h_pkg_hourly", + "period_start": "2026-11-03T12:00:00Z", + "period_end": "2026-11-03T13:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_h_pkg_hourly", + "period_start": "2026-11-03T13:00:00Z", + "period_end": "2026-11-03T14:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_h_pkg_hourly", + "period_start": "2026-11-03T10:00:00Z", + "period_end": "2026-11-03T11:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_h_pkg_hourly", + "period_start": "2026-11-03T11:00:00Z", + "period_end": "2026-11-03T12:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_h_pkg_hourly", + "period_start": "2026-11-03T12:00:00Z", + "period_end": "2026-11-03T13:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_h_pkg_hourly", + "period_start": "2026-11-03T13:00:00Z", + "period_end": "2026-11-03T14:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "monthly billing cycle", + "rule": "A fixed billing_cycle anchored at New York midnight: month boundaries move between 04:00Z (EDT) and 05:00Z (EST). The month containing the stop is owed in full.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-10-15T00:00:00Z", + "destination_method": { + "pattern": "file_transfer", + "transport": "s3" + }, + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "account_ids": [ + "acct_1" + ] + }, + "selector": { + "feed_purpose": "billing", + "period_duration": "P1M", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "consumer_receipt", + "destination": { + "destination_id": "billing-archive" + } + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-10-20T00:00:00Z", + "start_time": "2026-11-10T00:00:00Z", + "end_time": "2027-01-15T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_m_billing_monthly" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_m_billing_monthly", + "feed_purpose": "billing", + "report_definition_id": "rd_off_m_billing_monthly", + "report_definition_uri": "https://seller.example/report-definitions/off_m_billing_monthly.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1M", + "alignment": "billing_cycle", + "delivery_sla": "P5D", + "period_anchor_policy": "fixed", + "period_anchor": "2026-01-01T05:00:00Z", + "period_timezone": "America/New_York" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "consumer_receipt", + "standard_dimensions": [ + "package" + ], + "method": { + "pattern": "file_transfer", + "transport": "s3", + "orchestration": "producer_managed", + "destination_modes": [ + "existing" + ], + "formats": [ + "jsonl" + ] + } + } + ], + "now": "2027-03-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_m_billing_monthly", + "period_start": "2026-11-01T04:00:00Z", + "period_end": "2026-12-01T05:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_m_billing_monthly", + "period_start": "2026-12-01T05:00:00Z", + "period_end": "2027-01-01T05:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_m_billing_monthly", + "period_start": "2027-01-01T05:00:00Z", + "period_end": "2027-02-01T05:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_m_billing_monthly", + "period_start": "2026-11-01T04:00:00Z", + "period_end": "2026-12-01T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_m_billing_monthly", + "period_start": "2026-12-01T05:00:00Z", + "period_end": "2027-01-01T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_m_billing_monthly", + "period_start": "2027-01-01T05:00:00Z", + "period_end": "2027-02-01T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "ASAP buy confirmed mid-day owes the confirmation day", + "rule": "adcp#8593: an ASAP buy starts when it is confirmed, 11-03T14:30Z. The 11-03 period ends after confirmed_at and overlaps the flight, so it is owed in full.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-03T14:30:00Z", + "start_time": "2026-11-03T14:30:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "monthly billing buy confirmed on the 12th owes that month", + "rule": "adcp#8593: the version was accepted 10-15, so the start bound is the November boundary (11-01T04Z). The buy is confirmed and starts on 11-12; the November month ends after confirmed_at and overlaps the flight, so it is owed in full rather than starting in December.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-10-15T00:00:00Z", + "destination_method": { + "pattern": "file_transfer", + "transport": "s3" + }, + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "account_ids": [ + "acct_1" + ] + }, + "selector": { + "feed_purpose": "billing", + "period_duration": "P1M", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "consumer_receipt", + "destination": { + "destination_id": "billing-archive" + } + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-12T15:00:00Z", + "start_time": "2026-11-12T15:00:00Z", + "end_time": "2027-01-15T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_m_billing_monthly" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_m_billing_monthly", + "feed_purpose": "billing", + "report_definition_id": "rd_off_m_billing_monthly", + "report_definition_uri": "https://seller.example/report-definitions/off_m_billing_monthly.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1M", + "alignment": "billing_cycle", + "delivery_sla": "P5D", + "period_anchor_policy": "fixed", + "period_anchor": "2026-01-01T05:00:00Z", + "period_timezone": "America/New_York" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "consumer_receipt", + "standard_dimensions": [ + "package" + ], + "method": { + "pattern": "file_transfer", + "transport": "s3", + "orchestration": "producer_managed", + "destination_modes": [ + "existing" + ], + "formats": [ + "jsonl" + ] + } + } + ], + "now": "2027-03-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_m_billing_monthly", + "period_start": "2026-11-01T04:00:00Z", + "period_end": "2026-12-01T05:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_m_billing_monthly", + "period_start": "2026-12-01T05:00:00Z", + "period_end": "2027-01-01T05:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_m_billing_monthly", + "period_start": "2027-01-01T05:00:00Z", + "period_end": "2027-02-01T05:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_m_billing_monthly", + "period_start": "2026-11-01T04:00:00Z", + "period_end": "2026-12-01T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_m_billing_monthly", + "period_start": "2026-12-01T05:00:00Z", + "period_end": "2027-01-01T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_m_billing_monthly", + "period_start": "2027-01-01T05:00:00Z", + "period_end": "2027-02-01T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "destination selects managed offerings only", + "rule": "With a destination only offerings whose method matches its pattern and transport are candidates; without one only Core (method-less) offerings are.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only", + "destination": { + "destination_id": "archive", + "path_template": "a/{account_id}" + } + }, + "destination_method": { + "pattern": "file_transfer", + "transport": "gcs" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg", + "off_s3_pkg", + "off_t_gcs_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + }, + { + "offering_id": "off_s3_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_s3_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_s3_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ], + "method": { + "pattern": "file_transfer", + "transport": "s3", + "orchestration": "producer_managed", + "destination_modes": [ + "existing" + ], + "formats": [ + "jsonl" + ] + } + }, + { + "offering_id": "off_t_gcs_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_t_gcs_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_t_gcs_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ], + "method": { + "pattern": "file_transfer", + "transport": "gcs", + "orchestration": "producer_managed", + "destination_modes": [ + "existing" + ], + "formats": [ + "jsonl" + ] + } + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_t_gcs_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_t_gcs_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_t_gcs_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_t_gcs_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_t_gcs_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_t_gcs_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "multiple buys group into obligations", + "rule": "Each (buy, offering) pair is owed its own periods; obligations group them by period with frozen media_buy_ids. mb_0, confirmed 11-03T12, is owed the 11-03 period it was confirmed in (adcp#8593).", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + }, + { + "media_buy_id": "mb_0", + "confirmed_at": "2026-11-03T12:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-05T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_0", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_0", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_0", + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_0", + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "timezone cutover scoped to one offering", + "rule": "A timezone_cutovers entry with offering_id applies to that offering only; an offering with no applicable entry uses account.timezone (Los Angeles) throughout.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ], + "match": "all" + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/Los_Angeles", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T08:00:00Z", + "end_time": "2026-11-06T08:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_e_pkg_acct_tz", + "off_f_pkg_acct_tz" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_e_pkg_acct_tz", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_e_pkg_acct_tz", + "report_definition_uri": "https://seller.example/report-definitions/off_e_pkg_acct_tz.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "account_timezone", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + }, + { + "offering_id": "off_f_pkg_acct_tz", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_f_pkg_acct_tz", + "report_definition_uri": "https://seller.example/report-definitions/off_f_pkg_acct_tz.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "account_timezone", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z", + "coverage": { + "timezone_cutovers": [ + { + "offering_id": "off_e_pkg_acct_tz", + "effective_at": "2026-11-05T05:00:00Z", + "timezone": "America/Los_Angeles", + "previous_timezone": "America/New_York" + } + ] + } + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-03T05:00:00Z", + "period_end": "2026-11-04T05:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_f_pkg_acct_tz", + "period_start": "2026-11-03T08:00:00Z", + "period_end": "2026-11-04T08:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-04T05:00:00Z", + "period_end": "2026-11-05T05:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_f_pkg_acct_tz", + "period_start": "2026-11-04T08:00:00Z", + "period_end": "2026-11-05T08:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-05T08:00:00Z", + "period_end": "2026-11-06T08:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_f_pkg_acct_tz", + "period_start": "2026-11-05T08:00:00Z", + "period_end": "2026-11-06T08:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-03T05:00:00Z", + "period_end": "2026-11-04T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_f_pkg_acct_tz", + "period_start": "2026-11-03T08:00:00Z", + "period_end": "2026-11-04T08:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-04T05:00:00Z", + "period_end": "2026-11-05T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_f_pkg_acct_tz", + "period_start": "2026-11-04T08:00:00Z", + "period_end": "2026-11-05T08:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-05T08:00:00Z", + "period_end": "2026-11-06T08:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_f_pkg_acct_tz", + "period_start": "2026-11-05T08:00:00Z", + "period_end": "2026-11-06T08:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "gap scoped to one offering", + "rule": "A gaps[] entry with offering_id removes only that offering's periods; an entry without offering_id applies to every offering.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ], + "match": "all" + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg", + "off_b_pkg_creative" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + }, + { + "offering_id": "off_b_pkg_creative", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_b_pkg_creative", + "report_definition_uri": "https://seller.example/report-definitions/off_b_pkg_creative.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package", + "creative" + ] + } + ], + "now": "2026-12-01T00:00:00Z", + "coverage": { + "gaps": [ + { + "offering_id": "off_a_pkg", + "from": "2026-11-04T00:00:00Z", + "until": "2026-11-05T00:00:00Z", + "reason": "unauthorized" + } + ] + } + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_b_pkg_creative", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "match exact without dimensions", + "rule": "exact with no dimensions: every candidate qualifies and the lowest offering_id in byte order wins.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "match": "exact" + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg", + "off_b_pkg_creative", + "off_c_pkg_country", + "off_d_pkg_device" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + }, + { + "offering_id": "off_b_pkg_creative", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_b_pkg_creative", + "report_definition_uri": "https://seller.example/report-definitions/off_b_pkg_creative.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package", + "creative" + ] + }, + { + "offering_id": "off_c_pkg_country", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_c_pkg_country", + "report_definition_uri": "https://seller.example/report-definitions/off_c_pkg_country.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package", + "country" + ] + }, + { + "offering_id": "off_d_pkg_device", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_d_pkg_device", + "report_definition_uri": "https://seller.example/report-definitions/off_d_pkg_device.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ], + "unmapped_dimensions": [ + "device" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "period_timezone never matches an account-resolved source_timezone offering", + "rule": "A source_timezone offering matches a period_timezone override only when its published period_timezone equals it; an account_resolved offering publishes none.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "account_ids": [ + "acct_1" + ] + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ], + "period_timezone": "America/New_York" + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ], + "source_timezone": "America/New_York" + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_s_resolved" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_s_resolved", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_s_resolved", + "report_definition_uri": "https://seller.example/report-definitions/off_s_resolved.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "source_timezone", + "delivery_sla": "PT6H", + "period_timezone_policy": "account_resolved" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [], + "expected_obligations": [] + }, + { + "name": "period_timezone matches a fixed source_timezone offering in the same zone", + "rule": "A fixed source_timezone offering publishing America/New_York matches a New York override and keeps its own boundaries (05:00Z in November).", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "account_ids": [ + "acct_1" + ] + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ], + "period_timezone": "America/New_York" + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-05T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_s_fixed" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_s_fixed", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_s_fixed", + "report_definition_uri": "https://seller.example/report-definitions/off_s_fixed.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "source_timezone", + "delivery_sla": "PT6H", + "period_timezone_policy": "fixed", + "period_timezone": "America/New_York" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_s_fixed", + "period_start": "2026-11-02T05:00:00Z", + "period_end": "2026-11-03T05:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_s_fixed", + "period_start": "2026-11-03T05:00:00Z", + "period_end": "2026-11-04T05:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_s_fixed", + "period_start": "2026-11-04T05:00:00Z", + "period_end": "2026-11-05T05:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_s_fixed", + "period_start": "2026-11-02T05:00:00Z", + "period_end": "2026-11-03T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_s_fixed", + "period_start": "2026-11-03T05:00:00Z", + "period_end": "2026-11-04T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_s_fixed", + "period_start": "2026-11-04T05:00:00Z", + "period_end": "2026-11-05T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "a utc offering matches only UTC or Etc/UTC", + "rule": "period_timezone GMT is not one of the UTC aliases core/reporting-subscription.json names, so the utc offering does not match.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "account_ids": [ + "acct_1" + ] + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ], + "period_timezone": "GMT" + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [], + "expected_obligations": [] + }, + { + "name": "empty flight: canceled before start_time", + "rule": "canceled_at 11-05T02 is before start_time 11-05T06: the flight [start, stop) is empty and overlaps nothing, though the 11-05 period contains both instants.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-05T06:00:00Z", + "end_time": "2026-11-10T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ], + "canceled_at": "2026-11-05T02:00:00Z" + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [], + "expected_obligations": [] + }, + { + "name": "empty flight: stop equal to start", + "rule": "A zero-length flight (end_time equal to start_time at 11-03T12) is empty and overlaps nothing.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T12:00:00Z", + "end_time": "2026-11-03T12:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [], + "expected_obligations": [] + }, + { + "name": "two cutovers inside one period", + "rule": "v2 (11-04T12Z) switches to a Tokyo period_timezone and v3 (11-04T13Z) changes nothing else. v1 owes its New York day starting 11-04T05Z, through 11-05T05Z. v2 owes from that E and starts nothing before 13Z. v3 must carry v2's own start forward: it owes Tokyo days from the first boundary at or after 11-05T05Z (11-05T15Z), never the overlapping 11-04T15Z.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "superseded_at": "2026-11-04T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "account_ids": [ + "acct_1" + ] + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ], + "alignment": "account_timezone" + }, + "reconciliation_mode": "delivery_only" + } + }, + { + "subscription_version": 2, + "accepted_at": "2026-11-04T12:00:00Z", + "superseded_at": "2026-11-04T13:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 2, + "active": true, + "scope": { + "account_ids": [ + "acct_1" + ] + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ], + "alignment": "account_timezone", + "period_timezone": "Asia/Tokyo" + }, + "reconciliation_mode": "delivery_only" + } + }, + { + "subscription_version": 3, + "accepted_at": "2026-11-04T13:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 3, + "active": true, + "scope": { + "account_ids": [ + "acct_1" + ] + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ], + "alignment": "account_timezone", + "period_timezone": "Asia/Tokyo", + "match": "exact" + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-08T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_e_pkg_acct_tz" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_e_pkg_acct_tz", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_e_pkg_acct_tz", + "report_definition_uri": "https://seller.example/report-definitions/off_e_pkg_acct_tz.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "account_timezone", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-02T05:00:00Z", + "period_end": "2026-11-03T05:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-03T05:00:00Z", + "period_end": "2026-11-04T05:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-04T05:00:00Z", + "period_end": "2026-11-05T05:00:00Z" + }, + { + "subscription_version": 3, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-05T15:00:00Z", + "period_end": "2026-11-06T15:00:00Z" + }, + { + "subscription_version": 3, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-06T15:00:00Z", + "period_end": "2026-11-07T15:00:00Z" + }, + { + "subscription_version": 3, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-07T15:00:00Z", + "period_end": "2026-11-08T15:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-02T05:00:00Z", + "period_end": "2026-11-03T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-03T05:00:00Z", + "period_end": "2026-11-04T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-04T05:00:00Z", + "period_end": "2026-11-05T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 3, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-05T15:00:00Z", + "period_end": "2026-11-06T15:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 3, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-06T15:00:00Z", + "period_end": "2026-11-07T15:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 3, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-07T15:00:00Z", + "period_end": "2026-11-08T15:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "simultaneous version and timezone change at the same T", + "rule": "v2 is accepted at 11-05T05Z, exactly the timezone cutover effective_at (a New York boundary). v1 owes New York days starting before it; v2 owes Los Angeles days from the first boundary at or after it (11-05T08Z).", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ], + "alignment": "account_timezone" + }, + "reconciliation_mode": "delivery_only" + }, + "superseded_at": "2026-11-05T05:00:00Z" + }, + { + "subscription_version": 2, + "accepted_at": "2026-11-05T05:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 2, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ], + "alignment": "account_timezone" + }, + "reconciliation_mode": "delivery_only", + "exclude_account_ids": [ + "acct_other" + ] + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/Los_Angeles", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-08T08:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_e_pkg_acct_tz" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_e_pkg_acct_tz", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_e_pkg_acct_tz", + "report_definition_uri": "https://seller.example/report-definitions/off_e_pkg_acct_tz.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "account_timezone", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z", + "coverage": { + "timezone_cutovers": [ + { + "effective_at": "2026-11-05T05:00:00Z", + "timezone": "America/Los_Angeles", + "previous_timezone": "America/New_York" + } + ] + } + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-02T05:00:00Z", + "period_end": "2026-11-03T05:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-03T05:00:00Z", + "period_end": "2026-11-04T05:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-04T05:00:00Z", + "period_end": "2026-11-05T05:00:00Z" + }, + { + "subscription_version": 2, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-05T08:00:00Z", + "period_end": "2026-11-06T08:00:00Z" + }, + { + "subscription_version": 2, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-06T08:00:00Z", + "period_end": "2026-11-07T08:00:00Z" + }, + { + "subscription_version": 2, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-07T08:00:00Z", + "period_end": "2026-11-08T08:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-02T05:00:00Z", + "period_end": "2026-11-03T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-03T05:00:00Z", + "period_end": "2026-11-04T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-04T05:00:00Z", + "period_end": "2026-11-05T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 2, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-05T08:00:00Z", + "period_end": "2026-11-06T08:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 2, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-06T08:00:00Z", + "period_end": "2026-11-07T08:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 2, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-07T08:00:00Z", + "period_end": "2026-11-08T08:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "billing cycle anchored on the 31st clamps to month end", + "rule": "P1M from 2026-01-31 New York midnight: the 31st clamps to Feb 28 and returns to the 31st in March (a clamped February boundary does not shift the anchor day).", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-01-15T00:00:00Z", + "destination_method": { + "pattern": "file_transfer", + "transport": "s3" + }, + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "account_ids": [ + "acct_1" + ] + }, + "selector": { + "feed_purpose": "billing", + "period_duration": "P1M", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "consumer_receipt", + "destination": { + "destination_id": "billing-archive" + } + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2025-12-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-01-20T00:00:00Z", + "start_time": "2026-02-10T00:00:00Z", + "end_time": "2026-04-15T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_m31" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_m31", + "feed_purpose": "billing", + "report_definition_id": "rd_off_m31", + "report_definition_uri": "https://seller.example/report-definitions/off_m31.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1M", + "alignment": "billing_cycle", + "delivery_sla": "P5D", + "period_anchor_policy": "fixed", + "period_anchor": "2026-01-31T05:00:00Z", + "period_timezone": "America/New_York" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "consumer_receipt", + "standard_dimensions": [ + "package" + ], + "method": { + "pattern": "file_transfer", + "transport": "s3", + "orchestration": "producer_managed", + "destination_modes": [ + "existing" + ], + "formats": [ + "jsonl" + ] + } + } + ], + "now": "2026-06-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_m31", + "period_start": "2026-01-31T05:00:00Z", + "period_end": "2026-02-28T05:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_m31", + "period_start": "2026-02-28T05:00:00Z", + "period_end": "2026-03-31T04:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_m31", + "period_start": "2026-03-31T04:00:00Z", + "period_end": "2026-04-30T04:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_m31", + "period_start": "2026-01-31T05:00:00Z", + "period_end": "2026-02-28T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_m31", + "period_start": "2026-02-28T05:00:00Z", + "period_end": "2026-03-31T04:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_m31", + "period_start": "2026-03-31T04:00:00Z", + "period_end": "2026-04-30T04:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "billing cycle anchored on February 29", + "rule": "P1Y from 2024-02-29T00:00Z: non-leap years clamp to Feb 28 and 2028 returns to Feb 29.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2025-12-01T00:00:00Z", + "destination_method": { + "pattern": "file_transfer", + "transport": "s3" + }, + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "account_ids": [ + "acct_1" + ] + }, + "selector": { + "feed_purpose": "billing", + "period_duration": "P1Y", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "consumer_receipt", + "destination": { + "destination_id": "billing-archive" + } + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2025-11-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2025-12-15T00:00:00Z", + "start_time": "2026-01-01T00:00:00Z", + "end_time": "2027-06-01T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_y29" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_y29", + "feed_purpose": "billing", + "report_definition_id": "rd_off_y29", + "report_definition_uri": "https://seller.example/report-definitions/off_y29.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1Y", + "alignment": "billing_cycle", + "delivery_sla": "P5D", + "period_anchor_policy": "fixed", + "period_anchor": "2024-02-29T00:00:00Z", + "period_timezone": "UTC" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "consumer_receipt", + "standard_dimensions": [ + "package" + ], + "method": { + "pattern": "file_transfer", + "transport": "s3", + "orchestration": "producer_managed", + "destination_modes": [ + "existing" + ], + "formats": [ + "jsonl" + ] + } + } + ], + "now": "2028-06-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_y29", + "period_start": "2026-02-28T00:00:00Z", + "period_end": "2027-02-28T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_y29", + "period_start": "2027-02-28T00:00:00Z", + "period_end": "2028-02-29T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_y29", + "period_start": "2026-02-28T00:00:00Z", + "period_end": "2027-02-28T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_y29", + "period_start": "2027-02-28T00:00:00Z", + "period_end": "2028-02-29T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "DST fold at midnight (America/Santiago)", + "rule": "Chile falls back at 24:00 on 2026-04-04 to 23:00, so April 4 is a 25-hour local day; local midnight April 5 occurs once, at 04:00Z.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-03-01T00:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ], + "alignment": "account_timezone" + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/Santiago", + "grants": [ + { + "authorized_at": "2026-02-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-03-15T00:00:00Z", + "start_time": "2026-04-03T00:00:00Z", + "end_time": "2026-04-07T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_s_pkg_acct_tz" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_s_pkg_acct_tz", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_s_pkg_acct_tz", + "report_definition_uri": "https://seller.example/report-definitions/off_s_pkg_acct_tz.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "account_timezone", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-10-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_s_pkg_acct_tz", + "period_start": "2026-04-02T03:00:00Z", + "period_end": "2026-04-03T03:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_s_pkg_acct_tz", + "period_start": "2026-04-03T03:00:00Z", + "period_end": "2026-04-04T03:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_s_pkg_acct_tz", + "period_start": "2026-04-04T03:00:00Z", + "period_end": "2026-04-05T04:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_s_pkg_acct_tz", + "period_start": "2026-04-05T04:00:00Z", + "period_end": "2026-04-06T04:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_s_pkg_acct_tz", + "period_start": "2026-04-06T04:00:00Z", + "period_end": "2026-04-07T04:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_s_pkg_acct_tz", + "period_start": "2026-04-02T03:00:00Z", + "period_end": "2026-04-03T03:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_s_pkg_acct_tz", + "period_start": "2026-04-03T03:00:00Z", + "period_end": "2026-04-04T03:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_s_pkg_acct_tz", + "period_start": "2026-04-04T03:00:00Z", + "period_end": "2026-04-05T04:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_s_pkg_acct_tz", + "period_start": "2026-04-05T04:00:00Z", + "period_end": "2026-04-06T04:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_s_pkg_acct_tz", + "period_start": "2026-04-06T04:00:00Z", + "period_end": "2026-04-07T04:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "DST gap at midnight (America/Santiago)", + "rule": "Chile springs forward at 24:00 on 2026-09-05 to 01:00: local midnight September 6 does not exist and the boundary advances by the gap to 04:00Z, so September 6 is a 23-hour local day.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-03-01T00:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ], + "alignment": "account_timezone" + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/Santiago", + "grants": [ + { + "authorized_at": "2026-02-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-03-15T00:00:00Z", + "start_time": "2026-09-05T12:00:00Z", + "end_time": "2026-09-07T12:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_s_pkg_acct_tz" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_s_pkg_acct_tz", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_s_pkg_acct_tz", + "report_definition_uri": "https://seller.example/report-definitions/off_s_pkg_acct_tz.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "account_timezone", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-10-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_s_pkg_acct_tz", + "period_start": "2026-09-05T04:00:00Z", + "period_end": "2026-09-06T04:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_s_pkg_acct_tz", + "period_start": "2026-09-06T04:00:00Z", + "period_end": "2026-09-07T03:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_s_pkg_acct_tz", + "period_start": "2026-09-07T03:00:00Z", + "period_end": "2026-09-08T03:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_s_pkg_acct_tz", + "period_start": "2026-09-05T04:00:00Z", + "period_end": "2026-09-06T04:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_s_pkg_acct_tz", + "period_start": "2026-09-06T04:00:00Z", + "period_end": "2026-09-07T03:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_s_pkg_acct_tz", + "period_start": "2026-09-07T03:00:00Z", + "period_end": "2026-09-08T03:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "zero-length period on a skipped civil date (Pacific/Apia)", + "rule": "Samoa skipped 2011-12-30: that local day has zero length (its midnight and the next coincide at 10:00Z) and is never owed; the surrounding days are.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2011-12-01T00:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ], + "alignment": "account_timezone" + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "Pacific/Apia", + "grants": [ + { + "authorized_at": "2011-11-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2011-12-01T00:00:00Z", + "start_time": "2011-12-28T12:00:00Z", + "end_time": "2011-12-31T12:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_e_pkg_acct_tz" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_e_pkg_acct_tz", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_e_pkg_acct_tz", + "report_definition_uri": "https://seller.example/report-definitions/off_e_pkg_acct_tz.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "account_timezone", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2012-02-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2011-12-28T10:00:00Z", + "period_end": "2011-12-29T10:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2011-12-29T10:00:00Z", + "period_end": "2011-12-30T10:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2011-12-30T10:00:00Z", + "period_end": "2011-12-31T10:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2011-12-31T10:00:00Z", + "period_end": "2012-01-01T10:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2011-12-28T10:00:00Z", + "period_end": "2011-12-29T10:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2011-12-29T10:00:00Z", + "period_end": "2011-12-30T10:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2011-12-30T10:00:00Z", + "period_end": "2011-12-31T10:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2011-12-31T10:00:00Z", + "period_end": "2012-01-01T10:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "PT24H under a local alignment restarts at local midnight (#8588)", + "rule": "Under account_timezone a duration of only hours restarts at each local midnight (adcp#8588), so PT24H in New York across the 2026-03-08 spring-forward has a 23-hour period, exactly like P1D.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-03-01T00:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "PT24H", + "required_finality": "official", + "dimensions": [ + "package" + ], + "alignment": "account_timezone" + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-02-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-03-01T00:00:00Z", + "start_time": "2026-03-07T12:00:00Z", + "end_time": "2026-03-09T12:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_h24_acct_tz" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_h24_acct_tz", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_h24_acct_tz", + "report_definition_uri": "https://seller.example/report-definitions/off_h24_acct_tz.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "PT24H", + "alignment": "account_timezone", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-04-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_h24_acct_tz", + "period_start": "2026-03-07T05:00:00Z", + "period_end": "2026-03-08T05:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_h24_acct_tz", + "period_start": "2026-03-08T05:00:00Z", + "period_end": "2026-03-09T04:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_h24_acct_tz", + "period_start": "2026-03-09T04:00:00Z", + "period_end": "2026-03-10T04:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_h24_acct_tz", + "period_start": "2026-03-07T05:00:00Z", + "period_end": "2026-03-08T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_h24_acct_tz", + "period_start": "2026-03-08T05:00:00Z", + "period_end": "2026-03-09T04:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_h24_acct_tz", + "period_start": "2026-03-09T04:00:00Z", + "period_end": "2026-03-10T04:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "PT24H and P1D under utc are the same days", + "rule": "UTC has no DST, so PT24H and P1D yield identical boundaries under utc alignment.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "PT24H", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_u24" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_u24", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_u24", + "report_definition_uri": "https://seller.example/report-definitions/off_u24.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "PT24H", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_u24", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_u24", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_u24", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_u24", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_u24", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_u24", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "sub-daily local periods start on the local hour (Asia/Singapore)", + "rule": "adcp#8588: hourly periods under account_timezone are local wall-clock hours counted from each local midnight, not 1970 offsets (Singapore was +07:30 in 1970). confirmed_at 00:05Z falls in the 00:00Z (08:00 local) hour, which ends after it and overlaps the flight, so it is owed (adcp#8593).", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T00:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "pacing", + "period_duration": "PT1H", + "required_finality": "snapshot", + "dimensions": [ + "package" + ], + "alignment": "account_timezone" + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "Asia/Singapore", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-03T00:05:00Z", + "start_time": "2026-11-03T00:10:00Z", + "end_time": "2026-11-03T02:30:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_p_hourly_local" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_p_hourly_local", + "feed_purpose": "pacing", + "report_definition_id": "rd_off_p_hourly_local", + "report_definition_uri": "https://seller.example/report-definitions/off_p_hourly_local.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "PT1H", + "alignment": "account_timezone", + "delivery_sla": "PT1H" + }, + "supported_finality": [ + "snapshot" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-03T01:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-03T01:00:00Z", + "period_end": "2026-11-03T02:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-03T02:00:00Z", + "period_end": "2026-11-03T03:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-03T01:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-03T01:00:00Z", + "period_end": "2026-11-03T02:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-03T02:00:00Z", + "period_end": "2026-11-03T03:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "sub-daily local periods on a 25-hour day (America/New_York fall-back)", + "rule": "adcp#8588: the repeated 01:00 yields one boundary per offset, so 2026-11-01 in New York has 25 hourly periods.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-10-30T00:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "pacing", + "period_duration": "PT1H", + "required_finality": "snapshot", + "dimensions": [ + "package" + ], + "alignment": "account_timezone" + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-10-30T00:00:00Z", + "start_time": "2026-11-01T04:00:00Z", + "end_time": "2026-11-02T05:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_p_hourly_local" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_p_hourly_local", + "feed_purpose": "pacing", + "report_definition_id": "rd_off_p_hourly_local", + "report_definition_uri": "https://seller.example/report-definitions/off_p_hourly_local.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "PT1H", + "alignment": "account_timezone", + "delivery_sla": "PT1H" + }, + "supported_finality": [ + "snapshot" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T04:00:00Z", + "period_end": "2026-11-01T05:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T05:00:00Z", + "period_end": "2026-11-01T06:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T06:00:00Z", + "period_end": "2026-11-01T07:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T07:00:00Z", + "period_end": "2026-11-01T08:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T08:00:00Z", + "period_end": "2026-11-01T09:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T09:00:00Z", + "period_end": "2026-11-01T10:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T10:00:00Z", + "period_end": "2026-11-01T11:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T11:00:00Z", + "period_end": "2026-11-01T12:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T12:00:00Z", + "period_end": "2026-11-01T13:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T13:00:00Z", + "period_end": "2026-11-01T14:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T14:00:00Z", + "period_end": "2026-11-01T15:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T15:00:00Z", + "period_end": "2026-11-01T16:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T16:00:00Z", + "period_end": "2026-11-01T17:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T17:00:00Z", + "period_end": "2026-11-01T18:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T18:00:00Z", + "period_end": "2026-11-01T19:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T19:00:00Z", + "period_end": "2026-11-01T20:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T20:00:00Z", + "period_end": "2026-11-01T21:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T21:00:00Z", + "period_end": "2026-11-01T22:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T22:00:00Z", + "period_end": "2026-11-01T23:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T23:00:00Z", + "period_end": "2026-11-02T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-02T00:00:00Z", + "period_end": "2026-11-02T01:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-02T01:00:00Z", + "period_end": "2026-11-02T02:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-02T02:00:00Z", + "period_end": "2026-11-02T03:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-02T03:00:00Z", + "period_end": "2026-11-02T04:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-02T04:00:00Z", + "period_end": "2026-11-02T05:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T04:00:00Z", + "period_end": "2026-11-01T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T05:00:00Z", + "period_end": "2026-11-01T06:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T06:00:00Z", + "period_end": "2026-11-01T07:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T07:00:00Z", + "period_end": "2026-11-01T08:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T08:00:00Z", + "period_end": "2026-11-01T09:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T09:00:00Z", + "period_end": "2026-11-01T10:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T10:00:00Z", + "period_end": "2026-11-01T11:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T11:00:00Z", + "period_end": "2026-11-01T12:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T12:00:00Z", + "period_end": "2026-11-01T13:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T13:00:00Z", + "period_end": "2026-11-01T14:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T14:00:00Z", + "period_end": "2026-11-01T15:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T15:00:00Z", + "period_end": "2026-11-01T16:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T16:00:00Z", + "period_end": "2026-11-01T17:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T17:00:00Z", + "period_end": "2026-11-01T18:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T18:00:00Z", + "period_end": "2026-11-01T19:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T19:00:00Z", + "period_end": "2026-11-01T20:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T20:00:00Z", + "period_end": "2026-11-01T21:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T21:00:00Z", + "period_end": "2026-11-01T22:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T22:00:00Z", + "period_end": "2026-11-01T23:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-01T23:00:00Z", + "period_end": "2026-11-02T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-02T00:00:00Z", + "period_end": "2026-11-02T01:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-02T01:00:00Z", + "period_end": "2026-11-02T02:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-02T02:00:00Z", + "period_end": "2026-11-02T03:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-02T03:00:00Z", + "period_end": "2026-11-02T04:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_p_hourly_local", + "period_start": "2026-11-02T04:00:00Z", + "period_end": "2026-11-02T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "re-grant chain of three grants without backfill", + "rule": "Revoked 11-04T06 and re-granted 11-05T18, revoked 11-07T06 and re-granted 11-08T18, all inside retention: one chain with gaps [11-04, 11-06) and [11-07, 11-09).", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z", + "unauthorized_at": "2026-11-04T06:00:00Z" + }, + { + "authorized_at": "2026-11-05T18:00:00Z", + "unauthorized_at": "2026-11-07T06:00:00Z" + }, + { + "authorized_at": "2026-11-08T18:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-10T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z", + "status_retention_days": 30 + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-09T00:00:00Z", + "period_end": "2026-11-10T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-09T00:00:00Z", + "period_end": "2026-11-10T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "re-grant chain of three grants with backfill_on_regrant", + "rule": "The same chain with backfill_on_regrant: no gap, every period owed.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z", + "unauthorized_at": "2026-11-04T06:00:00Z" + }, + { + "authorized_at": "2026-11-05T18:00:00Z", + "unauthorized_at": "2026-11-07T06:00:00Z" + }, + { + "authorized_at": "2026-11-08T18:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-10T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z", + "status_retention_days": 30, + "backfill_on_regrant": true + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-07T00:00:00Z", + "period_end": "2026-11-08T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-08T00:00:00Z", + "period_end": "2026-11-09T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-09T00:00:00Z", + "period_end": "2026-11-10T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-07T00:00:00Z", + "period_end": "2026-11-08T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-08T00:00:00Z", + "period_end": "2026-11-09T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-09T00:00:00Z", + "period_end": "2026-11-10T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "re-grant seen only through the chain-start authorized_at", + "rule": "The wire keeps authorized_at at the chain start after a re-grant inside retention (adcp#8585), so the buyer lists the continuation with that value; the seller's gaps[] says which periods the interruption removed.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z", + "unauthorized_at": "2026-11-04T06:00:00Z" + }, + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-08T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z", + "status_retention_days": 30, + "coverage": { + "gaps": [ + { + "from": "2026-11-04T00:00:00Z", + "until": "2026-11-06T00:00:00Z", + "reason": "unauthorized" + } + ] + } + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-07T00:00:00Z", + "period_end": "2026-11-08T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-07T00:00:00Z", + "period_end": "2026-11-08T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "two authorization chains", + "rule": "Revoked 11-04T06; re-granted 11-10 after retention (1 day) expired, a new chain from 11-10; revoked again 11-12T06.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z", + "unauthorized_at": "2026-11-04T06:00:00Z" + }, + { + "authorized_at": "2026-11-10T00:00:00Z", + "unauthorized_at": "2026-11-12T06:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-15T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z", + "status_retention_days": 1 + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-10T00:00:00Z", + "period_end": "2026-11-11T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-11T00:00:00Z", + "period_end": "2026-11-12T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-10T00:00:00Z", + "period_end": "2026-11-11T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-11T00:00:00Z", + "period_end": "2026-11-12T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "match all adds an offering on a different alignment", + "rule": "v2 (11-04T12Z) moves from exact to all and adds a New York offering. The UTC offering continues (E = 11-05T00Z); the New York offering is new and starts at the latest outgoing end, so its first period is 11-05T05Z and 00Z-05Z is owed by neither.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + }, + "superseded_at": "2026-11-04T12:00:00Z" + }, + { + "subscription_version": 2, + "accepted_at": "2026-11-04T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 2, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ], + "match": "all" + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-07T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg", + "off_e_pkg_acct_tz" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + }, + { + "offering_id": "off_e_pkg_acct_tz", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_e_pkg_acct_tz", + "report_definition_uri": "https://seller.example/report-definitions/off_e_pkg_acct_tz.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "account_timezone", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 2, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + }, + { + "subscription_version": 2, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-05T05:00:00Z", + "period_end": "2026-11-06T05:00:00Z" + }, + { + "subscription_version": 2, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z" + }, + { + "subscription_version": 2, + "media_buy_id": "mb_1", + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-06T05:00:00Z", + "period_end": "2026-11-07T05:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 2, + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 2, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-05T05:00:00Z", + "period_end": "2026-11-06T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 2, + "offering_id": "off_a_pkg", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 2, + "offering_id": "off_e_pkg_acct_tz", + "period_start": "2026-11-06T05:00:00Z", + "period_end": "2026-11-07T05:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "version change exactly on a boundary", + "rule": "v2 accepted at 11-05T00Z, itself a boundary: E is T, v1 owes 11-03 and 11-04, v2 from 11-05.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + }, + "superseded_at": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 2, + "accepted_at": "2026-11-05T00:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 2, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only", + "exclude_account_ids": [ + "acct_other" + ] + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-07T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 2, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + }, + { + "subscription_version": 2, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 2, + "offering_id": "off_a_pkg", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 2, + "offering_id": "off_a_pkg", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "reactivation exactly on a boundary", + "rule": "Inactive [11-04T12, 11-06T00): 11-04 started before deactivation and stays owed; coverage resumes at 11-06 itself.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-08T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z", + "inactive_intervals": [ + { + "deactivated_at": "2026-11-04T12:00:00Z", + "reactivated_at": "2026-11-06T00:00:00Z" + } + ] + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-07T00:00:00Z", + "period_end": "2026-11-08T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-06T00:00:00Z", + "period_end": "2026-11-07T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-07T00:00:00Z", + "period_end": "2026-11-08T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "inactive interval spanning a version change", + "rule": "Deactivated under v1 at 11-04T12, v2 accepted while inactive at 11-05T12, reactivated 11-06T12: v1 owes 11-03 and 11-04, v2 starts fresh at 11-07.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + }, + "superseded_at": "2026-11-05T12:00:00Z" + }, + { + "subscription_version": 2, + "accepted_at": "2026-11-05T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 2, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only", + "exclude_account_ids": [ + "acct_other" + ] + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-09T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z", + "inactive_intervals": [ + { + "deactivated_at": "2026-11-04T12:00:00Z", + "reactivated_at": "2026-11-06T12:00:00Z" + } + ] + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 2, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-07T00:00:00Z", + "period_end": "2026-11-08T00:00:00Z" + }, + { + "subscription_version": 2, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-08T00:00:00Z", + "period_end": "2026-11-09T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 2, + "offering_id": "off_a_pkg", + "period_start": "2026-11-07T00:00:00Z", + "period_end": "2026-11-08T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 2, + "offering_id": "off_a_pkg", + "period_start": "2026-11-08T00:00:00Z", + "period_end": "2026-11-09T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "exception bounds between boundaries", + "rule": "capacity [11-04T12, 11-06T12) removes the periods whose start is in it, 11-05 and 11-06; 11-04 started before from and stays owed.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-08T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z", + "coverage": { + "excluded": [ + { + "reason": "capacity", + "from": "2026-11-04T12:00:00Z", + "until": "2026-11-06T12:00:00Z" + } + ] + } + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_a_pkg", + "period_start": "2026-11-07T00:00:00Z", + "period_end": "2026-11-08T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_a_pkg", + "period_start": "2026-11-07T00:00:00Z", + "period_end": "2026-11-08T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "finest tie in byte order (B before a)", + "rule": "Two offerings tie at two mapped dimensions; byte order puts uppercase B (0x42) before lowercase a (0x61), unlike locale order.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ], + "match": "finest" + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "a", + "B" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "a", + "feed_purpose": "analytics", + "report_definition_id": "rd_a", + "report_definition_uri": "https://seller.example/report-definitions/a.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package", + "country" + ] + }, + { + "offering_id": "B", + "feed_purpose": "analytics", + "report_definition_id": "rd_B", + "report_definition_uri": "https://seller.example/report-definitions/B.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package", + "creative" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "B", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "B", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "B", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "B", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "B", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "B", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "unknown dimension values parse and match only where published", + "rule": "The dimension vocabulary is extensible: an offering may publish placement, and a selector naming it matches that offering. A value no offering publishes (audience_segment) matches nothing.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package", + "placement" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_q_pkg_placement", + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_q_pkg_placement", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_q_pkg_placement", + "report_definition_uri": "https://seller.example/report-definitions/off_q_pkg_placement.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package", + "placement" + ] + }, + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [ + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_q_pkg_placement", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_q_pkg_placement", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z" + }, + { + "subscription_version": 1, + "media_buy_id": "mb_1", + "offering_id": "off_q_pkg_placement", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z" + } + ], + "expected_obligations": [ + { + "subscription_version": 1, + "offering_id": "off_q_pkg_placement", + "period_start": "2026-11-03T00:00:00Z", + "period_end": "2026-11-04T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_q_pkg_placement", + "period_start": "2026-11-04T00:00:00Z", + "period_end": "2026-11-05T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + }, + { + "subscription_version": 1, + "offering_id": "off_q_pkg_placement", + "period_start": "2026-11-05T00:00:00Z", + "period_end": "2026-11-06T00:00:00Z", + "media_buy_ids": [ + "mb_1" + ] + } + ] + }, + { + "name": "an unknown selector dimension no offering publishes matches nothing", + "rule": "A selector naming a value the seller does not map matches no offering.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package", + "audience_segment" + ], + "match": "all" + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg", + "off_b_pkg_creative", + "off_c_pkg_country", + "off_d_pkg_device" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + }, + { + "offering_id": "off_b_pkg_creative", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_b_pkg_creative", + "report_definition_uri": "https://seller.example/report-definitions/off_b_pkg_creative.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package", + "creative" + ] + }, + { + "offering_id": "off_c_pkg_country", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_c_pkg_country", + "report_definition_uri": "https://seller.example/report-definitions/off_c_pkg_country.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package", + "country" + ] + }, + { + "offering_id": "off_d_pkg_device", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_d_pkg_device", + "report_definition_uri": "https://seller.example/report-definitions/off_d_pkg_device.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ], + "unmapped_dimensions": [ + "device" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected": [], + "expected_obligations": [] + }, + { + "name": "numeric offset timezone is refused", + "rule": "iana_timezone: a numeric offset defines no DST rules.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "+05:00", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected_error": "TypeError" + }, + { + "name": "account_timezone offering requires the account timezone", + "rule": "Boundaries cannot be derived without the account timezone.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_e_pkg_acct_tz" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_e_pkg_acct_tz", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_e_pkg_acct_tz", + "report_definition_uri": "https://seller.example/report-definitions/off_e_pkg_acct_tz.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "account_timezone", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected_error": "TypeError" + }, + { + "name": "a timezone cutover without previous_timezone is refused", + "rule": "Sellers MUST include previous_timezone: account.timezone already shows the new zone, so the old schedule cannot be recovered without it.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_e_pkg_acct_tz" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_e_pkg_acct_tz", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_e_pkg_acct_tz", + "report_definition_uri": "https://seller.example/report-definitions/off_e_pkg_acct_tz.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "account_timezone", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z", + "coverage": { + "timezone_cutovers": [ + { + "effective_at": "2026-11-05T05:00:00Z", + "timezone": "America/Los_Angeles" + } + ] + } + }, + "expected_error": "TypeError" + }, + { + "name": "an offering without standard_dimensions cannot be resolved", + "rule": "Sellers advertising subscriptions MUST publish standard_dimensions on every offering; matching never guesses.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only" + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected_error": "TypeError" + }, + { + "name": "re-grant requires status_retention_days", + "rule": "Whether a re-grant continues coverage depends on the retention window.", + "input": { + "subscription_id": "analytics-daily", + "versions": [ + { + "subscription_version": 1, + "accepted_at": "2026-11-01T12:00:00Z", + "configuration": { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "active": true, + "scope": { + "all_authorized_accounts": true + }, + "selector": { + "feed_purpose": "analytics", + "period_duration": "P1D", + "required_finality": "official", + "dimensions": [ + "package" + ] + }, + "reconciliation_mode": "delivery_only" + } + } + ], + "account": { + "account_id": "acct_1", + "timezone": "America/New_York", + "grants": [ + { + "authorized_at": "2026-10-01T00:00:00Z", + "unauthorized_at": "2026-11-05T06:00:00Z" + }, + { + "authorized_at": "2026-11-06T18:00:00Z" + } + ] + }, + "buys": [ + { + "media_buy_id": "mb_1", + "confirmed_at": "2026-11-02T15:00:00Z", + "start_time": "2026-11-03T00:00:00Z", + "end_time": "2026-11-06T00:00:00Z", + "packages": [ + { + "package_id": "pkg_1", + "reporting_delivery_offering_ids": [ + "off_a_pkg" + ] + } + ] + } + ], + "offerings": [ + { + "offering_id": "off_a_pkg", + "feed_purpose": "analytics", + "report_definition_id": "rd_off_a_pkg", + "report_definition_uri": "https://seller.example/report-definitions/off_a_pkg.json", + "report_definition_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reporting_profile": { + "id": "adcp.delivery.v1", + "version": "1.0.0", + "schema_uri": "https://seller.example/schemas/delivery.json", + "schema_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "daily", + "primary_keys": [ + "account_id", + "package_id" + ] + }, + "schedule": { + "period_duration": "P1D", + "alignment": "utc", + "delivery_sla": "PT6H" + }, + "supported_finality": [ + "official" + ], + "reconciliation_mode": "delivery_only", + "standard_dimensions": [ + "package" + ] + } + ], + "now": "2026-12-01T00:00:00Z" + }, + "expected_error": "TypeError" + } + ] +} \ No newline at end of file diff --git a/tests/test_reporting_subscription_capabilities.py b/tests/test_reporting_subscription_capabilities.py new file mode 100644 index 000000000..520fcb6ae --- /dev/null +++ b/tests/test_reporting_subscription_capabilities.py @@ -0,0 +1,199 @@ +"""Capability fields for AdCP 3.3 reporting subscriptions. + +Each projected block is validated against the pinned 3.3.0-beta.1 +``core/reporting-delivery-capabilities.json``, whose ``allOf`` couples +``configuration_task``, ``configuration_tasks`` and ``subscriptions``. +""" + +from __future__ import annotations + +import pytest + +from adcp.reporting.subscriptions import ( + REPORTING_SUBSCRIPTIONS_FEATURE, + REPORTING_SUBSCRIPTIONS_SECTION, + ReportingSubscriptionSupport, + SubscriptionOffering, + advertise_reporting_subscriptions, + reporting_configuration_tasks, + subscription_capabilities, + with_subscription_dimensions, +) +from adcp.validation import schema_loader + +VERSION = "3.3.0-beta.1" + +BASE = { + "supported": True, + "reliable_reporting_version": "1.0", + "configuration_task": "sync_accounts", + "status_task": "get_reporting_status", + "revision_content_task": "get_media_buy_delivery", + "offerings": [ + { + "offering_id": "daily-official", + "feed_purpose": "analytics", + "report_definition_id": "delivery-v1", + "report_definition_uri": "https://seller.example/defs/delivery-v1.json", + "report_definition_sha256": "0" * 64, + "reporting_profile": { + "id": "delivery", + "version": "1", + "schema_uri": "https://seller.example/defs/delivery-v1.schema.json", + "schema_sha256": "0" * 64, + "schema_dialect": "https://json-schema.org/draft/2020-12/schema", + "schema_ref_policy": "local_fragment_only", + "grain": "package_day", + "primary_keys": ["account_id", "media_buy_id", "package_id", "period_start"], + }, + "schedule": { + "period_duration": "P1D", + "alignment": "account_timezone", + "delivery_sla": "PT6H", + }, + "supported_finality": ["official"], + "reconciliation_mode": "delivery_only", + } + ], + "automated_recovery_window_seconds": 21600, + "status_retention_days": 400, +} + + +PREVIEW_OFFERING_FIELDS = ("standard_dimensions", "unmapped_dimensions") + + +def _errors(block: dict) -> list[str]: + # TODO(3.3.0-beta.2): validate the adcp#8585 offering fields against the + # bundle once it carries them; beta.1 closes the offering object. + block = { + **block, + "offerings": [ + {k: v for k, v in item.items() if k not in PREVIEW_OFFERING_FIELDS} + for item in block["offerings"] + ], + } + validator = schema_loader.get_named_validator( + "core/reporting-delivery-capabilities.json", version=VERSION + ) + return [error.message for error in validator.iter_errors(block)] + + +def _published(block: dict) -> dict: + mapping = SubscriptionOffering.from_offering(block["offerings"][0], dimensions=("package",)) + return {**block, "offerings": [with_subscription_dimensions(block["offerings"][0], mapping)]} + + +def test_base_block_is_valid_and_unchanged_without_support() -> None: + assert _errors(BASE) == [] + assert subscription_capabilities(BASE, None) == BASE + assert advertise_reporting_subscriptions({"experimental_features": []}, None) == { + "experimental_features": [] + } + + +def test_subscriptions_alongside_account_configuration() -> None: + block = subscription_capabilities( + _published(BASE), + ReportingSubscriptionSupport( + max_accounts=500, backfill_on_regrant=True, dimensions=("package", "creative") + ), + ) + assert _errors(block) == [] + assert block["configuration_tasks"] == ["sync_principal", "sync_accounts"] + assert block["configuration_task"] == "sync_accounts" + assert block["subscriptions"] == { + "supported": True, + "max_accounts": 500, + "backfill_on_regrant": True, + "dimensions": ["package", "creative"], + } + + +def test_subscriptions_only_drops_the_deprecated_field() -> None: + block = subscription_capabilities( + _published(BASE), ReportingSubscriptionSupport(account_configuration=False) + ) + assert _errors(block) == [] + assert block["configuration_tasks"] == ["sync_principal"] + assert "configuration_task" not in block + assert block["subscriptions"] == {"supported": True} + + +def test_schema_rejects_the_shapes_the_helper_never_emits() -> None: + # configuration_task without sync_accounts listed, and subscriptions + # without sync_principal listed, are both invalid. + assert _errors( + {**BASE, "configuration_tasks": ["sync_principal"], "subscriptions": {"supported": True}} + ) + assert _errors({**BASE, "subscriptions": {"supported": True}}) + + +def test_capabilities_response_gets_feature_and_principal_section() -> None: + response = { + "experimental_features": ["media_buy.reporting_delivery", "protocol.principal"], + "adcp": { + "major_versions": [3], + "principal": { + "supported": True, + "sync_task": "sync_principal", + "read_task": "get_principal", + "supported_sections": ["reporting_destinations"], + }, + }, + } + advertised = advertise_reporting_subscriptions(response, ReportingSubscriptionSupport()) + assert advertised["experimental_features"] == [ + "media_buy.reporting_delivery", + "protocol.principal", + REPORTING_SUBSCRIPTIONS_FEATURE, + ] + assert advertised["adcp"]["principal"]["supported_sections"] == [ + "reporting_destinations", + REPORTING_SUBSCRIPTIONS_SECTION, + ] + assert advertise_reporting_subscriptions(advertised, ReportingSubscriptionSupport()) == ( + advertised + ) + assert response["adcp"]["principal"]["supported_sections"] == ["reporting_destinations"] + + +def test_configuration_tasks_and_support_validation() -> None: + assert reporting_configuration_tasks(subscriptions=False) == ["sync_accounts"] + with pytest.raises(ValueError): + reporting_configuration_tasks(subscriptions=False, account_configuration=False) + with pytest.raises(ValueError): + ReportingSubscriptionSupport(max_accounts=0) + with pytest.raises(ValueError): + ReportingSubscriptionSupport(dimensions=("Package",)) + support = ReportingSubscriptionSupport(dimensions=("package", "dma_zone", "package")) + assert support.dimensions == ("package", "dma_zone") + assert support.standard_dimensions == ("package",) + + +def test_offerings_must_publish_standard_dimensions() -> None: + with pytest.raises(ValueError, match="standard_dimensions"): + subscription_capabilities(BASE, ReportingSubscriptionSupport()) + published = _published(BASE)["offerings"][0] + assert published["standard_dimensions"] == ["package"] + assert "unmapped_dimensions" not in published + mapping = SubscriptionOffering.from_offering( + BASE["offerings"][0], dimensions=("package",), unmapped_dimensions=("device",) + ) + published = with_subscription_dimensions(BASE["offerings"][0], mapping) + assert published["unmapped_dimensions"] == ["device"] + # The published fields validate on the generated (patched) offering model + # and round-trip into the same mapping a buyer resolves from. + from adcp.types import ReportingDeliveryOffering + + model = ReportingDeliveryOffering.model_validate(published) + again = SubscriptionOffering.from_offering(model) + assert again.dimensions == frozenset({"package"}) + assert again.unmapped_dimensions == frozenset({"device"}) + + +def test_feature_id_without_the_principal_section_is_refused() -> None: + with pytest.raises(ValueError, match="adcp.principal"): + advertise_reporting_subscriptions( + {"experimental_features": []}, ReportingSubscriptionSupport() + ) diff --git a/tests/test_reporting_subscription_coverage.py b/tests/test_reporting_subscription_coverage.py new file mode 100644 index 000000000..cef1d43cd --- /dev/null +++ b/tests/test_reporting_subscription_coverage.py @@ -0,0 +1,1753 @@ +"""Table tests for the reporting-subscription coverage rule (AdCP 3.3). + +Every expected period below is written out by hand from the rule in +``core/reporting-subscription.json`` (coverage, cutover) and +``core/reporting-subscription-coverage.json``. The seller runtime owes exactly +what :func:`expected_subscription_periods` returns, so these tables are the +billing contract: change one only with a spec citation. + +Unless a test says otherwise the account is in ``America/New_York`` in +October 2026 (EDT, UTC-4), so a local day runs from 04:00Z to 04:00Z. +""" + +from __future__ import annotations + +import itertools +import random +from datetime import datetime, timedelta, timezone + +import pytest + +from adcp.reporting import ExpectedReportingPeriod +from adcp.reporting.subscriptions import ( + AccountTimezoneChange, + ExpectedSubscriptionPeriod, + SubscriptionAuthorization, + SubscriptionBuy, + SubscriptionBuyFlight, + SubscriptionBuyPackage, + SubscriptionDestination, + SubscriptionException, + SubscriptionOffering, + SubscriptionVersionWindow, + expected_subscription_periods, + resolve_subscription_offerings, + subscription_exceptions, + subscription_version_windows, +) +from adcp.types.domains.core.reporting_subscription_coverage import ( + ReportingSubscriptionCoverage, +) +from adcp.types.domains.core.reporting_subscription_state import ReportingSubscriptionState + +NY = "America/New_York" +UTC = timezone.utc + + +def at(text: str) -> datetime: + return datetime.fromisoformat(text.replace("Z", "+00:00")) + + +def offering( + offering_id: str, + *, + dimensions: tuple[str, ...] = ("package",), + unmapped: tuple[str, ...] = (), + duration: str = "P1D", + alignment: str = "account_timezone", + sla: str = "PT6H", + feed_purpose: str = "analytics", + finality: tuple[str, ...] = ("official",), + reconciliation_mode: str = "delivery_only", + method_pattern: str | None = None, + method_transport: str | None = None, + period_timezone: str | None = None, + period_anchor: datetime | None = None, + period_timezone_policy: str | None = None, + available_from: datetime | None = None, + withdrawn_at: datetime | None = None, +) -> SubscriptionOffering: + return SubscriptionOffering( + offering_id=offering_id, + report_definition_id=f"def-{offering_id}", + reporting_profile="profile-1", + feed_purpose=feed_purpose, + period_duration=duration, + alignment=alignment, + delivery_sla=sla, + supported_finality=frozenset(finality), + reconciliation_mode=reconciliation_mode, + dimensions=frozenset(dimensions), + unmapped_dimensions=frozenset(unmapped), + method_pattern=method_pattern, + method_transport=method_transport, + period_timezone=period_timezone, + period_anchor=period_anchor, + period_timezone_policy=period_timezone_policy, + available_from=available_from, + withdrawn_at=withdrawn_at, + ) + + +DAILY = offering("daily-pkg") +HOURLY_UTC = offering("hourly-pkg", duration="PT1H", alignment="utc", sla="PT2H") + + +def subscription_config( + *, + version: int = 1, + duration: str = "P1D", + dimensions: list[str] | None = None, + match: str | None = None, + scope: dict | None = None, + alignment: str | None = None, + exclude: list[str] | None = None, + operator_domains: list[str] | None = None, + destination: dict | None = None, + period_timezone: str | None = None, +) -> dict: + selector: dict = { + "feed_purpose": "analytics", + "period_duration": duration, + "required_finality": "official", + } + if dimensions is not None: + selector["dimensions"] = dimensions + if match is not None: + selector["match"] = match + if alignment is not None: + selector["alignment"] = alignment + if period_timezone is not None: + selector["period_timezone"] = period_timezone + config: dict = { + "subscription_id": "analytics-daily", + "subscription_version": version, + "active": True, + "scope": scope or {"all_authorized_accounts": True}, + "selector": selector, + "reconciliation_mode": "delivery_only", + } + if exclude: + config["exclude_account_ids"] = exclude + if operator_domains: + config["operator_domains"] = operator_domains + if destination: + config["destination"] = destination + return config + + +def state( + accepted_at: str, + *, + config: dict | None = None, + version: int = 1, + prior: list[tuple[int, str, str]] = (), + deactivated_at: str | None = None, +) -> dict: + payload: dict = { + "subscription_id": "analytics-daily", + "subscription_version": version, + "state": "inactive" if deactivated_at else "active", + "accepted_at": accepted_at, + "configuration": config or subscription_config(version=version), + } + if prior: + payload["prior_versions"] = [ + {"subscription_version": v, "accepted_at": a, "superseded_at": s} for v, a, s in prior + ] + if deactivated_at: + payload["deactivated_at"] = deactivated_at + return payload + + +def buy( + media_buy_id: str = "mb-1", + *, + confirmed_at: str = "2026-10-01T00:00:00Z", + start_time: str = "2026-10-01T04:00:00Z", + end_time: str = "2026-10-31T04:00:00Z", + canceled_at: str | None = None, + completed_at: str | None = None, + offering_ids: tuple[str, ...] = ("daily-pkg",), + packages: tuple[SubscriptionBuyPackage, ...] = (), + superseded: tuple[SubscriptionBuyFlight, ...] = (), +) -> SubscriptionBuy: + return SubscriptionBuy( + media_buy_id=media_buy_id, + confirmed_at=at(confirmed_at) if confirmed_at else None, + start_time=at(start_time), + end_time=at(end_time), + canceled_at=at(canceled_at) if canceled_at else None, + completed_at=at(completed_at) if completed_at else None, + reporting_delivery_offering_ids=offering_ids, + packages=packages, + superseded_flights=superseded, + ) + + +def periods(**kwargs) -> list[tuple[str, str]]: + return [(item.period_start, item.period_end) for item in run(**kwargs)] + + +def starts(**kwargs) -> list[str]: + return [item.period_start for item in run(**kwargs)] + + +def run(**kwargs) -> list[ExpectedSubscriptionPeriod]: + kwargs.setdefault("subscription", state("2026-10-01T00:00:00Z")) + kwargs.setdefault("account_id", "acct-1") + kwargs.setdefault("offerings", [DAILY, HOURLY_UTC]) + kwargs.setdefault("account_timezone", NY) + kwargs.setdefault("now", at("2026-12-31T00:00:00Z")) + return expected_subscription_periods(**kwargs) + + +def days(first: str, count: int) -> list[str]: + """``count`` consecutive EDT local-midnight starts from ``first`` (YYYY-MM-DD).""" + day = datetime.fromisoformat(first) + return [(day + timedelta(days=index)).strftime("%Y-%m-%dT04:00:00Z") for index in range(count)] + + +# -- the start rule ------------------------------------------------------------ + + +class TestCoverageStart: + def test_whole_flight_after_acceptance(self) -> None: + result = run(buys=[buy(end_time="2026-10-04T04:00:00Z")]) + assert [(p.period_start, p.period_end) for p in result] == [ + ("2026-10-01T04:00:00Z", "2026-10-02T04:00:00Z"), + ("2026-10-02T04:00:00Z", "2026-10-03T04:00:00Z"), + ("2026-10-03T04:00:00Z", "2026-10-04T04:00:00Z"), + ] + first = result[0] + assert isinstance(first, ExpectedReportingPeriod) + assert first.delivery_config_id == "analytics-daily" + assert first.delivery_config_version == 1 + assert first.offering_id == "daily-pkg" + assert first.report_definition_id == "def-daily-pkg" + assert first.source_timezone == NY + assert first.media_buy_ids == ("mb-1",) + assert first.expected_at == "2026-10-02T10:00:00Z" + assert first.withheld is False + + def test_mid_period_confirmation_owes_that_period_in_full(self) -> None: + # adcp#8593: confirmed 11:00 local on Oct 5, with a flight that began + # Oct 3. A buy is owed every period that ends after its confirmed_at, + # so the Oct 5 local day is owed in full; Oct 3 and 4 are not. + result = starts( + buys=[ + buy( + confirmed_at="2026-10-05T15:00:00Z", + start_time="2026-10-03T04:00:00Z", + end_time="2026-10-08T04:00:00Z", + ) + ] + ) + assert result == days("2026-10-05", 3) + + def test_confirmation_on_a_boundary_owes_that_period(self) -> None: + result = starts( + buys=[ + buy( + confirmed_at="2026-10-05T04:00:00Z", + start_time="2026-10-03T04:00:00Z", + end_time="2026-10-07T04:00:00Z", + ) + ] + ) + assert result == days("2026-10-05", 2) + + def test_mid_period_acceptance_starts_at_next_boundary(self) -> None: + result = starts( + subscription=state("2026-10-02T12:00:00Z"), + buys=[buy(end_time="2026-10-05T04:00:00Z")], + ) + assert result == days("2026-10-03", 2) + + def test_authorization_later_than_acceptance_and_confirmation(self) -> None: + result = starts( + buys=[buy(end_time="2026-10-06T04:00:00Z")], + authorizations=[SubscriptionAuthorization(at("2026-10-03T09:00:00Z"))], + ) + assert result == days("2026-10-04", 2) + + def test_confirmed_early_with_a_later_flight_owes_nothing_before_the_flight(self) -> None: + # A buy confirmed Oct 1 with an Oct 20 start owes nothing before Oct 20. + result = starts( + buys=[ + buy( + confirmed_at="2026-10-01T00:00:00Z", + start_time="2026-10-20T04:00:00Z", + end_time="2026-10-22T04:00:00Z", + ) + ] + ) + assert result == days("2026-10-20", 2) + + def test_mid_period_flight_start_owes_the_whole_containing_period(self) -> None: + # No clipping: a flight starting at 15:00Z overlaps the Oct 10 local day. + result = periods( + buys=[buy(start_time="2026-10-10T15:00:00Z", end_time="2026-10-11T15:00:00Z")] + ) + assert result == [ + ("2026-10-10T04:00:00Z", "2026-10-11T04:00:00Z"), + ("2026-10-11T04:00:00Z", "2026-10-12T04:00:00Z"), + ] + + def test_unconfirmed_buy_is_not_covered(self) -> None: + unconfirmed = SubscriptionBuy( + media_buy_id="mb-1", + confirmed_at=None, + start_time=at("2026-10-01T04:00:00Z"), + end_time=at("2026-10-03T04:00:00Z"), + reporting_delivery_offering_ids=("daily-pkg",), + ) + assert run(buys=[unconfirmed]) == [] + + def test_periods_are_owed_only_once_resolved(self) -> None: + now = at("2026-10-03T10:00:00Z") + assert starts(buys=[buy()], now=now) == days("2026-10-01", 2) + unresolved = run( + buys=[buy(end_time="2026-10-05T04:00:00Z")], now=now, include_unresolved=True + ) + assert [p.period_start for p in unresolved] == days("2026-10-01", 4) + + +# -- the stop rule ------------------------------------------------------------- + + +class TestCoverageStop: + def test_end_time_on_a_boundary(self) -> None: + assert starts(buys=[buy(end_time="2026-10-03T04:00:00Z")]) == days("2026-10-01", 2) + + def test_end_time_mid_period_owes_that_period_in_full(self) -> None: + result = periods(buys=[buy(end_time="2026-10-03T10:00:00Z")]) + assert result[-1] == ("2026-10-03T04:00:00Z", "2026-10-04T04:00:00Z") + assert len(result) == 3 + + def test_cancellation_mid_flight(self) -> None: + # A buy canceled mid-period owes that period and none after it. + result = starts( + buys=[buy(canceled_at="2026-10-04T17:30:00Z", end_time="2026-10-31T04:00:00Z")] + ) + assert result == days("2026-10-01", 4) + + def test_cancellation_on_a_boundary_owes_nothing_from_it(self) -> None: + result = starts(buys=[buy(canceled_at="2026-10-04T04:00:00Z")]) + assert result == days("2026-10-01", 3) + + def test_early_completion(self) -> None: + result = starts(buys=[buy(completed_at="2026-10-02T23:00:00Z")]) + assert result == days("2026-10-01", 2) + + def test_earliest_of_end_cancel_and_complete_wins(self) -> None: + result = starts( + buys=[ + buy( + end_time="2026-10-06T04:00:00Z", + canceled_at="2026-10-05T12:00:00Z", + completed_at="2026-10-03T12:00:00Z", + ) + ] + ) + assert result == days("2026-10-01", 3) + + def test_canceled_before_start_owes_nothing(self) -> None: + result = run( + buys=[ + buy( + start_time="2026-10-10T04:00:00Z", + canceled_at="2026-10-05T00:00:00Z", + ) + ] + ) + assert result == [] + + +class TestFlightAsOfResolution: + def test_extension_adds_only_unresolved_periods(self) -> None: + # Flight ended Oct 4; on Oct 7 at 12:00Z it was extended to Oct 10. + # Periods that resolved before the extension (Oct 4..6 local days) + # are never owed retroactively; the Oct 7 day resolves after it. + extended = buy( + end_time="2026-10-10T04:00:00Z", + superseded=( + SubscriptionBuyFlight( + superseded_at=at("2026-10-07T12:00:00Z"), + start_time=at("2026-10-01T04:00:00Z"), + end_time=at("2026-10-04T04:00:00Z"), + ), + ), + ) + assert starts(buys=[extended]) == [*days("2026-10-01", 3), *days("2026-10-07", 3)] + + def test_extension_effective_exactly_at_resolution_is_visible(self) -> None: + extended = buy( + end_time="2026-10-06T04:00:00Z", + superseded=( + SubscriptionBuyFlight( + superseded_at=at("2026-10-05T04:00:00Z"), + start_time=at("2026-10-01T04:00:00Z"), + end_time=at("2026-10-04T04:00:00Z"), + ), + ), + ) + # Oct 4 local day resolves at Oct 5 04:00Z, the instant of the change. + assert starts(buys=[extended]) == days("2026-10-01", 5) + + def test_reinstatement_never_revives_elapsed_periods(self) -> None: + reinstated = buy( + end_time="2026-10-08T04:00:00Z", + superseded=( + SubscriptionBuyFlight( + superseded_at=at("2026-10-06T00:00:00Z"), + start_time=at("2026-10-01T04:00:00Z"), + end_time=at("2026-10-08T04:00:00Z"), + canceled_at=at("2026-10-03T00:00:00Z"), + ), + ), + ) + # Canceled during the Oct 2 local day; reinstated Oct 6 00:00Z (Oct 5 + # local). The Oct 5 day resolves after the reinstatement. + assert starts(buys=[reinstated]) == [*days("2026-10-01", 2), *days("2026-10-05", 3)] + + def test_shortened_flight_keeps_already_resolved_periods(self) -> None: + shortened = buy( + end_time="2026-10-03T04:00:00Z", + superseded=( + SubscriptionBuyFlight( + superseded_at=at("2026-10-06T00:00:00Z"), + start_time=at("2026-10-01T04:00:00Z"), + end_time=at("2026-10-31T04:00:00Z"), + ), + ), + ) + # Days through Oct 4 resolved before the change at Oct 6 00:00Z. + assert starts(buys=[shortened]) == days("2026-10-01", 4) + + +# -- versions and cutovers ----------------------------------------------------- + + +class TestVersionCutovers: + def test_new_version_same_schedule_is_seamless(self) -> None: + subscription = state( + "2026-10-03T12:00:00Z", + version=2, + config=subscription_config(version=2), + prior=[(1, "2026-10-01T00:00:00Z", "2026-10-03T12:00:00Z")], + ) + result = run(subscription=subscription, buys=[buy(end_time="2026-10-06T04:00:00Z")]) + assert [(p.period_start, p.delivery_config_version) for p in result] == [ + ("2026-10-01T04:00:00Z", 1), + ("2026-10-02T04:00:00Z", 1), + ("2026-10-03T04:00:00Z", 1), # started before T: old version, in full + ("2026-10-04T04:00:00Z", 2), # E = 2026-10-04T04:00Z + ("2026-10-05T04:00:00Z", 2), + ] + + def test_new_version_on_a_boundary(self) -> None: + subscription = state( + "2026-10-03T04:00:00Z", + version=2, + config=subscription_config(version=2), + prior=[(1, "2026-10-01T00:00:00Z", "2026-10-03T04:00:00Z")], + ) + result = run(subscription=subscription, buys=[buy(end_time="2026-10-05T04:00:00Z")]) + assert [(p.period_start, p.delivery_config_version) for p in result] == [ + ("2026-10-01T04:00:00Z", 1), + ("2026-10-02T04:00:00Z", 1), + ("2026-10-03T04:00:00Z", 2), + ("2026-10-04T04:00:00Z", 2), + ] + + def test_daily_to_hourly_cutover_with_misaligned_boundaries(self) -> None: + # v1 resolves the local-day offering, v2 the UTC-hourly one. T is + # 2026-10-02T09:30Z: v1 owes the Oct 2 local day in full, ending at + # E = 2026-10-03T04:00Z; v2 owes hours starting at or after E. + subscription = state( + "2026-10-02T09:30:00Z", + version=2, + config=subscription_config(version=2, duration="PT1H"), + prior=[(1, "2026-10-01T00:00:00Z", "2026-10-02T09:30:00Z")], + ) + result = run( + subscription=subscription, + version_configurations={1: subscription_config(version=1)}, + buys=[ + buy( + end_time="2026-10-03T07:00:00Z", + offering_ids=("daily-pkg", "hourly-pkg"), + ) + ], + ) + assert [(p.period_start, p.period_end, p.offering_id) for p in result] == [ + ("2026-10-01T04:00:00Z", "2026-10-02T04:00:00Z", "daily-pkg"), + ("2026-10-02T04:00:00Z", "2026-10-03T04:00:00Z", "daily-pkg"), + ("2026-10-03T04:00:00Z", "2026-10-03T05:00:00Z", "hourly-pkg"), + ("2026-10-03T05:00:00Z", "2026-10-03T06:00:00Z", "hourly-pkg"), + ("2026-10-03T06:00:00Z", "2026-10-03T07:00:00Z", "hourly-pkg"), + ] + + def test_hourly_to_daily_cutover_leaves_an_unowed_stretch(self) -> None: + # T = 10:30Z: v1 owes the 10:00-11:00 hour in full (E = 11:00Z). The + # first local day starting at or after E begins at 04:00Z next day; + # 11:00Z..04:00Z is owed under neither schedule. + subscription = state( + "2026-10-02T10:30:00Z", + version=2, + config=subscription_config(version=2), + prior=[(1, "2026-10-02T08:00:00Z", "2026-10-02T10:30:00Z")], + ) + result = run( + subscription=subscription, + version_configurations={1: subscription_config(version=1, duration="PT1H")}, + buys=[ + buy( + end_time="2026-10-04T04:00:00Z", + offering_ids=("daily-pkg", "hourly-pkg"), + ) + ], + ) + assert [(p.period_start, p.period_end, p.delivery_config_version) for p in result] == [ + ("2026-10-02T08:00:00Z", "2026-10-02T09:00:00Z", 1), + ("2026-10-02T09:00:00Z", "2026-10-02T10:00:00Z", 1), + ("2026-10-02T10:00:00Z", "2026-10-02T11:00:00Z", 1), + ("2026-10-03T04:00:00Z", "2026-10-04T04:00:00Z", 2), + ] + + def test_account_added_by_a_new_version_follows_the_ordinary_start(self) -> None: + subscription = state( + "2026-10-03T12:00:00Z", + version=2, + config=subscription_config(version=2, scope={"account_ids": ["acct-1", "acct-2"]}), + prior=[(1, "2026-10-01T00:00:00Z", "2026-10-03T12:00:00Z")], + ) + result = run( + subscription=subscription, + version_configurations={1: subscription_config(scope={"account_ids": ["acct-2"]})}, + buys=[buy(end_time="2026-10-06T04:00:00Z")], + ) + assert [(p.period_start, p.delivery_config_version) for p in result] == [ + ("2026-10-04T04:00:00Z", 2), + ("2026-10-05T04:00:00Z", 2), + ] + + def test_account_dropped_by_a_new_version_keeps_started_periods(self) -> None: + subscription = state( + "2026-10-03T12:00:00Z", + version=2, + config=subscription_config(version=2, scope={"account_ids": ["acct-2"]}), + prior=[(1, "2026-10-01T00:00:00Z", "2026-10-03T12:00:00Z")], + ) + result = run( + subscription=subscription, + version_configurations={1: subscription_config()}, + buys=[buy(end_time="2026-10-06T04:00:00Z")], + ) + assert [p.period_start for p in result] == days("2026-10-01", 3) + + def test_deactivation_keeps_started_periods_and_creates_no_later_ones(self) -> None: + subscription = state("2026-10-01T00:00:00Z", deactivated_at="2026-10-03T12:00:00Z") + assert starts(subscription=subscription, buys=[buy()]) == days("2026-10-01", 3) + + def test_deactivation_on_a_boundary(self) -> None: + subscription = state("2026-10-01T00:00:00Z", deactivated_at="2026-10-03T04:00:00Z") + assert starts(subscription=subscription, buys=[buy()]) == days("2026-10-01", 2) + + def test_three_versions_back_to_back(self) -> None: + subscription = state( + "2026-10-02T06:00:00Z", + version=3, + config=subscription_config(version=3), + prior=[ + (2, "2026-10-02T05:00:00Z", "2026-10-02T06:00:00Z"), + (1, "2026-10-01T00:00:00Z", "2026-10-02T05:00:00Z"), + ], + ) + result = run(subscription=subscription, buys=[buy(end_time="2026-10-05T04:00:00Z")]) + # v2 is in effect for one hour inside the Oct 2 day, which v1 already + # owes in full; the next boundary belongs to v3. + assert [(p.period_start, p.delivery_config_version) for p in result] == [ + ("2026-10-01T04:00:00Z", 1), + ("2026-10-02T04:00:00Z", 1), + ("2026-10-03T04:00:00Z", 3), + ("2026-10-04T04:00:00Z", 3), + ] + + def test_version_windows_from_a_generated_state_model(self) -> None: + model = ReportingSubscriptionState.model_validate( + state( + "2026-10-03T12:00:00Z", + version=2, + config=subscription_config(version=2), + prior=[(1, "2026-10-01T00:00:00Z", "2026-10-03T12:00:00Z")], + ) + ) + windows = subscription_version_windows(model) + assert [(w.subscription_version, w.accepted_at, w.ends_at) for w in windows] == [ + (1, at("2026-10-01T00:00:00Z"), at("2026-10-03T12:00:00Z")), + (2, at("2026-10-03T12:00:00Z"), None), + ] + from_model = run(subscription=model, buys=[buy(end_time="2026-10-06T04:00:00Z")]) + from_windows = run(subscription=windows, buys=[buy(end_time="2026-10-06T04:00:00Z")]) + assert from_model == from_windows + + def test_out_of_order_versions_are_rejected(self) -> None: + with pytest.raises(ValueError): + subscription_version_windows( + state( + "2026-10-03T12:00:00Z", + version=2, + prior=[(1, "2026-10-01T00:00:00Z", "2026-10-04T00:00:00Z")], + ) + ) + + +# -- resolution ---------------------------------------------------------------- + + +PACKAGE = offering("off-b-package", dimensions=("package",)) +PACKAGE_CREATIVE = offering("off-a-pkg-creative", dimensions=("package", "creative")) +PACKAGE_COUNTRY = offering("off-c-pkg-country", dimensions=("package", "country")) +PACKAGE_DEVICE = offering("off-d-pkg-device", dimensions=("package",), unmapped=("device",)) +BREAKDOWNS = [PACKAGE, PACKAGE_CREATIVE, PACKAGE_COUNTRY, PACKAGE_DEVICE] +ALL_IDS = tuple(item.offering_id for item in BREAKDOWNS) + + +def resolved(config: dict, *, product: tuple[str, ...] = ALL_IDS, **kwargs) -> tuple[str, ...]: + return resolve_subscription_offerings( + config, kwargs.pop("offerings", BREAKDOWNS), product_offering_ids=product, **kwargs + ).offering_ids + + +class TestResolution: + def test_exact_is_the_default_with_dimensions(self) -> None: + assert resolved(subscription_config(dimensions=["package"])) == ("off-b-package",) + + def test_unmapped_native_dimensions_count_as_distinct(self) -> None: + # package x device does not equal package. + only_device = (PACKAGE_DEVICE.offering_id,) + assert resolved(subscription_config(dimensions=["package"]), product=only_device) == () + + def test_finest_is_the_default_without_dimensions_and_ties_go_to_lowest_id(self) -> None: + # Two offerings have two mapped dimensions; byte order picks "off-a...". + assert resolved(subscription_config()) == ("off-a-pkg-creative",) + + def test_finest_with_dimensions(self) -> None: + config = subscription_config(dimensions=["package"], match="finest") + assert resolved(config) == ("off-a-pkg-creative",) + config = subscription_config(dimensions=["country"], match="finest") + assert resolved(config) == ("off-c-pkg-country",) + + def test_all_selects_every_including_offering_in_byte_order(self) -> None: + config = subscription_config(dimensions=["package"], match="all") + assert resolved(config) == ( + "off-a-pkg-creative", + "off-b-package", + "off-c-pkg-country", + "off-d-pkg-device", + ) + + def test_exact_ties_go_to_the_lowest_offering_id_in_byte_order(self) -> None: + upper = offering("Z-package") + lower = offering("a-package") + result = resolved( + subscription_config(dimensions=["package"]), + offerings=[lower, upper], + product=("a-package", "Z-package"), + ) + assert result == ("Z-package",) # b"Z" < b"a" + + def test_only_offerings_the_product_lists_are_candidates(self) -> None: + config = subscription_config(dimensions=["package"], match="all") + assert resolved(config, product=("off-b-package",)) == ("off-b-package",) + + def test_unknown_vocabulary_value_matches_nothing(self) -> None: + assert resolved(subscription_config(dimensions=["dma_zone"])) == () + + def test_period_duration_finality_purpose_and_mode_must_match(self) -> None: + assert resolved(subscription_config(duration="PT1H")) == () + snapshot_only = offering("snap", finality=("snapshot",)) + assert resolved(subscription_config(), offerings=[snapshot_only], product=("snap",)) == () + billing = offering("bill", feed_purpose="billing") + assert resolved(subscription_config(), offerings=[billing], product=("bill",)) == () + receipt = offering("rcpt", reconciliation_mode="consumer_receipt") + assert resolved(subscription_config(), offerings=[receipt], product=("rcpt",)) == () + + def test_alignment_filter(self) -> None: + utc_daily = offering("utc-daily", alignment="utc") + both = [DAILY, utc_daily] + product = ("daily-pkg", "utc-daily") + config = subscription_config(dimensions=["package"], alignment="utc") + assert resolved(config, offerings=both, product=product) == ("utc-daily",) + + def test_destination_selects_managed_offerings_only(self) -> None: + managed = offering("managed", method_pattern="file_transfer", method_transport="s3") + both = [DAILY, managed] + product = ("daily-pkg", "managed") + core = subscription_config(dimensions=["package"]) + assert resolved(core, offerings=both, product=product) == ("daily-pkg",) + config = subscription_config( + dimensions=["package"], destination={"destination_id": "warehouse"} + ) + s3 = SubscriptionDestination(pattern="file_transfer", transport="s3") + gcs = SubscriptionDestination(pattern="file_transfer", transport="gcs") + assert resolved(config, offerings=both, product=product, destination=s3) == ("managed",) + assert resolved(config, offerings=both, product=product, destination=gcs) == () + with pytest.raises(ValueError): + resolved(config, offerings=both, product=product) + + def test_period_timezone_override(self) -> None: + # account_timezone and account-resolved schedules can produce periods + # in any zone; fixed-zone schedules only in theirs; utc only in UTC. + source = offering( + "src", + alignment="source_timezone", + period_timezone="America/Chicago", + period_timezone_policy="fixed", + ) + utc_daily = offering("utc-daily", alignment="utc") + offers = [DAILY, source, utc_daily] + product = ("daily-pkg", "src", "utc-daily") + + def override(zone: str) -> dict: + return subscription_config( + dimensions=["package"], + match="all", + scope={"account_ids": ["acct-1"]}, + period_timezone=zone, + ) + + assert resolved(override("America/Chicago"), offerings=offers, product=product) == ( + "daily-pkg", + "src", + ) + assert resolved(override("Etc/UTC"), offerings=offers, product=product) == ( + "daily-pkg", + "utc-daily", + ) + # The override moves an account_timezone schedule into its zone. + result = periods( + subscription=state("2026-10-01T00:00:00Z", config=override("America/Chicago")), + offerings=[DAILY], + buys=[buy(end_time="2026-10-03T05:00:00Z")], + ) + assert result[0] == ("2026-10-01T05:00:00Z", "2026-10-02T05:00:00Z") + + def test_configurable_billing_cycles_are_never_candidates(self) -> None: + # A subscription has no way to supply a billing anchor. + configurable = offering("cycle", alignment="billing_cycle") + assert resolved(subscription_config(), offerings=[configurable], product=("cycle",)) == () + monthly = offering("monthly", duration="P1M") + config = subscription_config(duration="P1M") + assert resolved(config, offerings=[monthly], product=("monthly",)) == ("monthly",) + + def test_from_wire_offering(self) -> None: + wire = { + "offering_id": "gam-daily", + "feed_purpose": "analytics", + "report_definition_id": "gam-delivery-v1", + "report_definition_uri": "https://seller.example/defs/gam-delivery-v1.json", + "report_definition_sha256": "0" * 64, + "reporting_profile": {"id": "delivery", "version": "1"}, + "schedule": { + "period_duration": "P1D", + "alignment": "source_timezone", + "period_timezone_policy": "fixed", + "period_timezone": "America/Los_Angeles", + "delivery_sla": "PT12H", + }, + "supported_finality": ["snapshot", "official"], + "reconciliation_mode": "delivery_only", + } + projected = SubscriptionOffering.from_offering(wire, dimensions=("package", "creative")) + assert projected.reporting_profile == "delivery" + assert projected.zone(NY) == "America/Los_Angeles" + assert projected.schedulable + start, end = next( + iter( + (p.period_start, p.period_end) + for p in run( + offerings=[projected], + subscription=state( + "2026-10-01T00:00:00Z", + config=subscription_config(dimensions=["package", "creative"]), + ), + buys=[buy(offering_ids=("gam-daily",), end_time="2026-10-03T07:00:00Z")], + ) + ) + ) + # Local midnight in Los Angeles (PDT, UTC-7). + assert (start, end) == ("2026-10-01T07:00:00Z", "2026-10-02T07:00:00Z") + + +class TestResolutionInCoverage: + def test_all_owes_each_offering_its_own_periods(self) -> None: + config = subscription_config(dimensions=["package"], match="all") + result = run( + subscription=state("2026-10-01T00:00:00Z", config=config), + offerings=[PACKAGE, PACKAGE_CREATIVE], + buys=[ + buy( + end_time="2026-10-03T04:00:00Z", + offering_ids=(PACKAGE.offering_id, PACKAGE_CREATIVE.offering_id), + ) + ], + ) + assert [(p.period_start, p.offering_id) for p in result] == [ + ("2026-10-01T04:00:00Z", "off-a-pkg-creative"), + ("2026-10-01T04:00:00Z", "off-b-package"), + ("2026-10-02T04:00:00Z", "off-a-pkg-creative"), + ("2026-10-02T04:00:00Z", "off-b-package"), + ] + + def test_a_new_finer_offering_takes_over_from_the_next_boundary(self) -> None: + # Under finest, a finer offering published at 2026-10-03T12:00Z is a + # resolution change: the old offering keeps the Oct 3 day. + finer = offering( + "off-a-pkg-creative", + dimensions=("package", "creative"), + available_from=at("2026-10-03T12:00:00Z"), + ) + config = subscription_config(dimensions=["package"], match="finest") + result = run( + subscription=state("2026-10-01T00:00:00Z", config=config), + offerings=[PACKAGE, finer], + buys=[ + buy( + end_time="2026-10-06T04:00:00Z", + offering_ids=(PACKAGE.offering_id, finer.offering_id), + ) + ], + ) + assert [(p.period_start, p.offering_id) for p in result] == [ + ("2026-10-01T04:00:00Z", "off-b-package"), + ("2026-10-02T04:00:00Z", "off-b-package"), + ("2026-10-03T04:00:00Z", "off-b-package"), + ("2026-10-04T04:00:00Z", "off-a-pkg-creative"), + ("2026-10-05T04:00:00Z", "off-a-pkg-creative"), + ] + + def test_withdrawn_offering_with_no_successor_stops_after_started_periods(self) -> None: + withdrawn = offering("daily-pkg", withdrawn_at=at("2026-10-03T12:00:00Z")) + result = starts(offerings=[withdrawn], buys=[buy()]) + assert result == days("2026-10-01", 3) + + def test_packages_resolve_separately_and_union_per_buy(self) -> None: + config = subscription_config(dimensions=["package"]) + mixed = buy( + end_time="2026-10-02T04:00:00Z", + packages=( + SubscriptionBuyPackage("pkg-1", (PACKAGE.offering_id,)), + SubscriptionBuyPackage("pkg-2", (PACKAGE_CREATIVE.offering_id,)), + ), + ) + result = run( + subscription=state("2026-10-01T00:00:00Z", config=config), + offerings=[PACKAGE, PACKAGE_CREATIVE], + buys=[mixed], + ) + # pkg-2 has no exact match: the buy stays covered for pkg-1. + assert [(p.period_start, p.offering_id) for p in result] == [ + ("2026-10-01T04:00:00Z", "off-b-package"), + ] + + def test_buys_share_one_expected_period_per_offering(self) -> None: + result = run( + buys=[ + buy("mb-2", end_time="2026-10-03T04:00:00Z"), + buy("mb-1", end_time="2026-10-02T04:00:00Z"), + ] + ) + assert [(p.period_start, p.media_buy_ids) for p in result] == [ + ("2026-10-01T04:00:00Z", ("mb-1", "mb-2")), + ("2026-10-02T04:00:00Z", ("mb-2",)), + ] + + def test_scope_filters(self) -> None: + excluded = state("2026-10-01T00:00:00Z", config=subscription_config(exclude=["acct-1"])) + assert run(subscription=excluded, buys=[buy()]) == [] + named = state( + "2026-10-01T00:00:00Z", config=subscription_config(scope={"account_ids": ["acct-2"]}) + ) + assert run(subscription=named, buys=[buy()]) == [] + operator = state( + "2026-10-01T00:00:00Z", + config=subscription_config(operator_domains=["agency.example"]), + ) + assert run(subscription=operator, buys=[buy()], account_operator="other.example") == [] + assert run(subscription=operator, buys=[buy()], account_operator="agency.example") + # An account whose operator is unknown is not selected. + assert run(subscription=operator, buys=[buy()]) == [] + + +# -- exceptions ---------------------------------------------------------------- + + +class TestExceptions: + def test_account_level_exception_removes_periods_in_from_until(self) -> None: + result = starts( + buys=[buy(end_time="2026-10-08T04:00:00Z")], + exceptions=[ + { + "reason": "capacity", + "from": "2026-10-03T04:00:00Z", + "until": "2026-10-05T04:00:00Z", + } + ], + ) + assert result == [*days("2026-10-01", 2), *days("2026-10-05", 3)] + + def test_open_exception_removes_everything_from_it(self) -> None: + result = starts( + buys=[buy(end_time="2026-10-08T04:00:00Z")], + exceptions=[ + { + "reason": "overridden", + "from": "2026-10-04T04:00:00Z", + "overridden_by": {"delivery_config_id": "acme"}, + } + ], + ) + assert result == days("2026-10-01", 3) + + def test_buy_level_exception_only_touches_that_buy(self) -> None: + result = run( + buys=[ + buy("mb-1", end_time="2026-10-03T04:00:00Z"), + buy("mb-2", end_time="2026-10-03T04:00:00Z"), + ], + exceptions=[ + { + "media_buy_id": "mb-2", + "reason": "offering_unsupported", + "from": "2026-10-02T04:00:00Z", + } + ], + ) + assert [(p.period_start, p.media_buy_ids) for p in result] == [ + ("2026-10-01T04:00:00Z", ("mb-1", "mb-2")), + ("2026-10-02T04:00:00Z", ("mb-1",)), + ] + + def test_package_exceptions_never_remove_periods(self) -> None: + exceptions = [ + { + "media_buy_id": "mb-1", + "reason": "offering_unsupported", + "from": "2026-10-01T04:00:00Z", + "package_ids": ["pkg-9"], + } + ] + with_exception = run(buys=[buy(end_time="2026-10-03T04:00:00Z")], exceptions=exceptions) + assert with_exception == run(buys=[buy(end_time="2026-10-03T04:00:00Z")]) + + def test_generated_coverage_model_round_trips(self) -> None: + coverage = ReportingSubscriptionCoverage.model_validate( + { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "status": "active", + "excluded": [ + { + "reason": "capacity", + "from": "2026-10-03T04:00:00Z", + "until": "2026-10-04T04:00:00Z", + } + ], + "gaps": [ + { + "from": "2026-10-05T04:00:00Z", + "until": "2026-10-06T04:00:00Z", + "reason": "unauthorized", + } + ], + } + ) + normalized = subscription_exceptions(coverage.excluded, coverage.gaps or ()) + assert [(e.reason, e.from_, e.until) for e in normalized] == [ + ("capacity", at("2026-10-03T04:00:00Z"), at("2026-10-04T04:00:00Z")), + ("unauthorized", at("2026-10-05T04:00:00Z"), at("2026-10-06T04:00:00Z")), + ] + result = starts( + buys=[buy(end_time="2026-10-07T04:00:00Z")], + exceptions=coverage.excluded, + gaps=coverage.gaps or (), + ) + assert result == [*days("2026-10-01", 2), "2026-10-04T04:00:00Z", "2026-10-06T04:00:00Z"] + + def test_exceptions_only_ever_remove_periods(self) -> None: + rng = random.Random(8543) + buys = [ + buy("mb-1", end_time="2026-10-09T04:00:00Z"), + buy("mb-2", start_time="2026-10-03T10:00:00Z", end_time="2026-10-12T04:00:00Z"), + ] + baseline = run(buys=buys) + owed = {(p.period_start, b) for p in baseline for b in p.media_buy_ids} + instants = [ + f"2026-10-{day:02d}T{hour:02d}:00:00Z" for day in range(1, 13) for hour in (0, 4, 13) + ] + for _ in range(200): + entries = [] + for _ in range(rng.randint(1, 4)): + lo, hi = sorted(rng.sample(instants, 2)) + entry: dict = { + "reason": rng.choice(["capacity", "excluded_by_buyer", "offering_unsupported"]), + "from": lo, + } + if rng.random() < 0.7: + entry["until"] = hi + if rng.random() < 0.5: + entry["media_buy_id"] = rng.choice(["mb-1", "mb-2"]) + entry["reason"] = "offering_unsupported" + entries.append(entry) + trimmed = run(buys=buys, exceptions=entries) + remaining = {(p.period_start, b) for p in trimmed for b in p.media_buy_ids} + assert remaining <= owed + removed = owed - remaining + for start, media_buy_id in removed: + assert any( + e["from"] <= start + and ("until" not in e or start < e["until"]) + and e.get("media_buy_id", media_buy_id) == media_buy_id + for e in entries + ) + + +# -- lost authority -------------------------------------------------------------- + + +GRANT = at("2026-10-01T00:00:00Z") + + +class TestLostAuthority: + def test_revocation_still_in_effect(self) -> None: + # Lost at 2026-10-04T10:00Z: the Oct 4 day started before the loss + # but ends after it, so it is not owed. Oct 3 ended before the loss + # but its expected_at (Oct 4 10:00Z) is not after the loss, so it + # stays due at its original deadline. + result = run( + buys=[buy(end_time="2026-10-08T04:00:00Z")], + authorizations=[SubscriptionAuthorization(GRANT, at("2026-10-04T10:00:00Z"))], + ) + assert [(p.period_start, p.expected_at, p.withheld) for p in result] == [ + ("2026-10-01T04:00:00Z", "2026-10-02T10:00:00Z", False), + ("2026-10-02T04:00:00Z", "2026-10-03T10:00:00Z", False), + ("2026-10-03T04:00:00Z", "2026-10-04T10:00:00Z", False), + ] + + def test_period_pending_at_the_loss_is_withheld(self) -> None: + result = run( + buys=[buy(end_time="2026-10-08T04:00:00Z")], + authorizations=[SubscriptionAuthorization(GRANT, at("2026-10-04T08:00:00Z"))], + ) + assert [(p.period_start, p.expected_at, p.withheld) for p in result] == [ + ("2026-10-01T04:00:00Z", "2026-10-02T10:00:00Z", False), + ("2026-10-02T04:00:00Z", "2026-10-03T10:00:00Z", False), + ("2026-10-03T04:00:00Z", None, True), + ] + + def test_regrant_without_backfill_records_a_gap(self) -> None: + # Lost 2026-10-04T08:00Z, re-granted 2026-10-06T15:00Z. Gap from the + # last boundary at or before the loss (Oct 4 04:00Z) until the first + # boundary at or after the re-grant (Oct 7 04:00Z). + result = run( + buys=[buy(end_time="2026-10-09T04:00:00Z")], + status_retention_days=30, + authorizations=[ + SubscriptionAuthorization(GRANT, at("2026-10-04T08:00:00Z")), + SubscriptionAuthorization(at("2026-10-06T15:00:00Z")), + ], + ) + assert [(p.period_start, p.expected_at) for p in result] == [ + ("2026-10-01T04:00:00Z", "2026-10-02T10:00:00Z"), + ("2026-10-02T04:00:00Z", "2026-10-03T10:00:00Z"), + # withheld at the loss, due at re-grant + delivery_sla + ("2026-10-03T04:00:00Z", "2026-10-06T21:00:00Z"), + ("2026-10-07T04:00:00Z", "2026-10-08T10:00:00Z"), + ("2026-10-08T04:00:00Z", "2026-10-09T10:00:00Z"), + ] + assert not any(p.withheld for p in result) + + def test_regrant_with_backfill_keeps_the_revoked_interval_owed(self) -> None: + result = run( + buys=[buy(end_time="2026-10-09T04:00:00Z")], + backfill_on_regrant=True, + status_retention_days=30, + authorizations=[ + SubscriptionAuthorization(GRANT, at("2026-10-04T08:00:00Z")), + SubscriptionAuthorization(at("2026-10-06T15:00:00Z")), + ], + ) + assert [(p.period_start, p.expected_at) for p in result] == [ + ("2026-10-01T04:00:00Z", "2026-10-02T10:00:00Z"), + ("2026-10-02T04:00:00Z", "2026-10-03T10:00:00Z"), + ("2026-10-03T04:00:00Z", "2026-10-06T21:00:00Z"), + ("2026-10-04T04:00:00Z", "2026-10-06T21:00:00Z"), + ("2026-10-05T04:00:00Z", "2026-10-06T21:00:00Z"), + ("2026-10-06T04:00:00Z", "2026-10-07T10:00:00Z"), + ("2026-10-07T04:00:00Z", "2026-10-08T10:00:00Z"), + ("2026-10-08T04:00:00Z", "2026-10-09T10:00:00Z"), + ] + + def test_regrant_after_retention_is_a_fresh_start_without_backfill(self) -> None: + result = run( + buys=[buy(end_time="2026-10-09T04:00:00Z")], + backfill_on_regrant=True, + status_retention_days=1, + authorizations=[ + SubscriptionAuthorization(GRANT, at("2026-10-04T08:00:00Z")), + SubscriptionAuthorization(at("2026-10-06T15:00:00Z")), + ], + ) + assert [(p.period_start, p.expected_at, p.withheld) for p in result] == [ + ("2026-10-01T04:00:00Z", "2026-10-02T10:00:00Z", False), + ("2026-10-02T04:00:00Z", "2026-10-03T10:00:00Z", False), + ("2026-10-03T04:00:00Z", None, True), + ("2026-10-07T04:00:00Z", "2026-10-08T10:00:00Z", False), + ("2026-10-08T04:00:00Z", "2026-10-09T10:00:00Z", False), + ] + + def test_loss_on_a_boundary(self) -> None: + result = starts( + buys=[buy(end_time="2026-10-09T04:00:00Z")], + status_retention_days=30, + authorizations=[ + SubscriptionAuthorization(GRANT, at("2026-10-04T04:00:00Z")), + SubscriptionAuthorization(at("2026-10-06T04:00:00Z")), + ], + ) + assert result == [*days("2026-10-01", 3), *days("2026-10-06", 3)] + + def test_regrant_needs_status_retention_days(self) -> None: + with pytest.raises(ValueError): + run( + buys=[buy()], + authorizations=[ + SubscriptionAuthorization(GRANT, at("2026-10-04T08:00:00Z")), + SubscriptionAuthorization(at("2026-10-06T15:00:00Z")), + ], + ) + + def test_regrant_exactly_at_retention_is_a_fresh_start(self) -> None: + # A re-grant continues the chain only before unauthorized_at + retention. + kwargs = dict( + buys=[buy(end_time="2026-10-09T04:00:00Z")], + backfill_on_regrant=True, + status_retention_days=2, + ) + inside = run( + authorizations=[ + SubscriptionAuthorization(GRANT, at("2026-10-04T08:00:00Z")), + SubscriptionAuthorization(at("2026-10-06T07:59:59Z")), + ], + **kwargs, + ) + at_limit = run( + authorizations=[ + SubscriptionAuthorization(GRANT, at("2026-10-04T08:00:00Z")), + SubscriptionAuthorization(at("2026-10-06T08:00:00Z")), + ], + **kwargs, + ) + assert "2026-10-04T04:00:00Z" in [p.period_start for p in inside] + assert "2026-10-04T04:00:00Z" not in [p.period_start for p in at_limit] + + def test_a_grant_repeating_the_chain_start_continues_the_chain(self) -> None: + # adcp#8585: list_accounts keeps authorized_at at the chain start after + # a re-grant inside retention, so the re-grant instant is unknown and + # the seller's gaps[] decide what the interruption removed. + result = starts( + buys=[buy(end_time="2026-10-08T04:00:00Z")], + authorizations=[ + SubscriptionAuthorization(GRANT, at("2026-10-04T08:00:00Z")), + SubscriptionAuthorization(GRANT), + ], + gaps=[ + { + "from": "2026-10-04T04:00:00Z", + "until": "2026-10-06T04:00:00Z", + "reason": "unauthorized", + } + ], + ) + assert result == [*days("2026-10-01", 3), *days("2026-10-06", 2)] + with pytest.raises(ValueError): + run( + buys=[buy()], + authorizations=[SubscriptionAuthorization(GRANT), SubscriptionAuthorization(GRANT)], + ) + + +# -- timezones and clocks -------------------------------------------------------- + + +class TestTimezones: + def test_account_timezone_sets_the_boundaries(self) -> None: + result = periods( + account_timezone="America/Los_Angeles", + buys=[buy(start_time="2026-10-01T07:00:00Z", end_time="2026-10-03T07:00:00Z")], + ) + assert result == [ + ("2026-10-01T07:00:00Z", "2026-10-02T07:00:00Z"), + ("2026-10-02T07:00:00Z", "2026-10-03T07:00:00Z"), + ] + + def test_daily_periods_follow_local_midnight_across_fall_back(self) -> None: + # 2026-11-01 is the US fall-back day: it lasts 25 hours. + result = periods( + subscription=state("2026-10-30T00:00:00Z"), + buys=[ + buy( + confirmed_at="2026-10-30T00:00:00Z", + start_time="2026-10-31T04:00:00Z", + end_time="2026-11-03T05:00:00Z", + ) + ], + ) + assert result == [ + ("2026-10-31T04:00:00Z", "2026-11-01T04:00:00Z"), + ("2026-11-01T04:00:00Z", "2026-11-02T05:00:00Z"), + ("2026-11-02T05:00:00Z", "2026-11-03T05:00:00Z"), + ] + + def test_local_hourly_periods_skip_the_spring_forward_gap(self) -> None: + local_hourly = offering("local-hourly", duration="PT1H", sla="PT1H") + result = periods( + subscription=state("2026-03-08T00:00:00Z", config=subscription_config(duration="PT1H")), + offerings=[local_hourly], + buys=[ + buy( + confirmed_at="2026-03-08T00:00:00Z", + start_time="2026-03-08T05:00:00Z", + end_time="2026-03-08T08:00:00Z", + offering_ids=("local-hourly",), + ) + ], + ) + # 00:00 EST, 01:00 EST, then 03:00 EDT: the 02:00 slot has no length. + assert result == [ + ("2026-03-08T05:00:00Z", "2026-03-08T06:00:00Z"), + ("2026-03-08T06:00:00Z", "2026-03-08T07:00:00Z"), + ("2026-03-08T07:00:00Z", "2026-03-08T08:00:00Z"), + ] + + def test_timezone_change_is_a_cutover(self) -> None: + # The spec's example: a daily feed moves from UTC-5 to UTC-8 at 12:00Z. + # The old schedule ends at 05:00Z the next day; the first new-timezone + # day starts at 08:00Z; 05:00Z..08:00Z is owed under neither. + result = run( + account_timezone="Etc/GMT+8", # the current zone, as list_accounts shows it + timezone_changes=[ + AccountTimezoneChange( + at("2026-10-03T12:00:00Z"), "Etc/GMT+8", previous_timezone="Etc/GMT+5" + ) + ], + buys=[buy(start_time="2026-10-02T05:00:00Z", end_time="2026-10-05T08:00:00Z")], + ) + assert [(p.period_start, p.period_end, p.source_timezone) for p in result] == [ + ("2026-10-02T05:00:00Z", "2026-10-03T05:00:00Z", "Etc/GMT+5"), + ("2026-10-03T05:00:00Z", "2026-10-04T05:00:00Z", "Etc/GMT+5"), + ("2026-10-04T08:00:00Z", "2026-10-05T08:00:00Z", "Etc/GMT+8"), + ] + + def test_timezone_change_does_not_touch_utc_offerings(self) -> None: + kwargs = dict( + subscription=state("2026-10-03T00:00:00Z", config=subscription_config(duration="PT1H")), + buys=[ + buy( + start_time="2026-10-03T10:00:00Z", + end_time="2026-10-03T14:00:00Z", + offering_ids=("hourly-pkg",), + ) + ], + ) + moved = run( + timezone_changes=[ + AccountTimezoneChange(at("2026-10-03T12:30:00Z"), "Etc/GMT+8", previous_timezone=NY) + ], + **kwargs, + ) + assert moved == run(**kwargs) + + def test_billing_cycle_anchor(self) -> None: + cycle = offering( + "weekly-cycle", + duration="P7D", + alignment="billing_cycle", + period_timezone=NY, + period_anchor=at("2026-09-28T04:00:00Z"), # a Monday, local midnight + ) + result = periods( + subscription=state("2026-10-01T00:00:00Z", config=subscription_config(duration="P7D")), + offerings=[cycle], + buys=[buy(end_time="2026-10-13T04:00:00Z", offering_ids=("weekly-cycle",))], + ) + assert result == [ + ("2026-10-05T04:00:00Z", "2026-10-12T04:00:00Z"), + ("2026-10-12T04:00:00Z", "2026-10-19T04:00:00Z"), + ] + + def test_naive_timestamps_are_rejected(self) -> None: + with pytest.raises(ValueError): + run(buys=[buy()], now=datetime(2026, 12, 1)) + + +def test_explicit_windows_and_exception_values() -> None: + windows = [ + SubscriptionVersionWindow(1, at("2026-10-01T00:00:00Z"), None, subscription_config()) + ] + result = run( + subscription=windows, + buys=[buy(end_time="2026-10-04T04:00:00Z")], + exceptions=[ + SubscriptionException( + "capacity", at("2026-10-02T04:00:00Z"), at("2026-10-03T04:00:00Z") + ) + ], + ) + assert [p.period_start for p in result] == ["2026-10-01T04:00:00Z", "2026-10-03T04:00:00Z"] + + +@pytest.mark.parametrize( + "confirmed, accepted, granted", + list( + itertools.product( + ["2026-10-02T00:00:00Z", "2026-10-02T04:00:00Z", "2026-10-03T13:00:00Z"], + ["2026-10-01T00:00:00Z", "2026-10-03T04:00:00Z", "2026-10-03T23:59:00Z"], + ["2026-09-01T00:00:00Z", "2026-10-02T16:00:00Z", "2026-10-04T04:00:00Z"], + ) + ), +) +def test_start_is_the_first_boundary_after_the_latest_of_three( + confirmed: str, accepted: str, granted: str +) -> None: + # adcp#8593: periods start at or after the later of accepted_at and + # authorized_at, and end after confirmed_at. + bound = max(at(accepted), at(granted)) + expected = [ + start + for start in days("2026-09-30", 10) + if at(start) >= bound + and at(start) + timedelta(days=1) > at(confirmed) + and at(start) < at("2026-10-08T04:00:00Z") + ] + result = starts( + subscription=state(accepted), + authorizations=[SubscriptionAuthorization(at(granted))], + buys=[ + buy( + confirmed_at=confirmed, + start_time="2026-09-30T04:00:00Z", + end_time="2026-10-08T04:00:00Z", + ) + ], + ) + assert result == expected + + +# -- fields settled by adcp#8585 ---------------------------------------------------- + + +class TestSettledFields: + def test_readback_inactive_intervals_are_a_fresh_start(self) -> None: + readback = state("2026-10-01T00:00:00Z") + readback["inactive_intervals"] = [ + {"deactivated_at": "2026-10-03T12:00:00Z", "reactivated_at": "2026-10-05T12:00:00Z"} + ] + model = ReportingSubscriptionState.model_validate(readback) + assert starts(subscription=model, buys=[buy(end_time="2026-10-08T04:00:00Z")]) == [ + *days("2026-10-01", 3), + *days("2026-10-06", 2), + ] + + def test_timezone_cutover_from_coverage_with_previous_timezone(self) -> None: + coverage = ReportingSubscriptionCoverage.model_validate( + { + "subscription_id": "analytics-daily", + "subscription_version": 1, + "status": "active", + "excluded": [], + "timezone_cutovers": [ + { + "effective_at": "2026-10-04T05:00:00Z", + "timezone": "Etc/GMT+8", + "previous_timezone": "Etc/GMT+5", + } + ], + } + ) + # account_timezone is the current zone; before the cutover the zone is + # previous_timezone. + result = run( + account_timezone="Etc/GMT+8", + coverage=coverage, + buys=[buy(start_time="2026-10-02T05:00:00Z", end_time="2026-10-05T08:00:00Z")], + ) + assert [(p.period_start, p.source_timezone) for p in result] == [ + ("2026-10-02T05:00:00Z", "Etc/GMT+5"), + ("2026-10-03T05:00:00Z", "Etc/GMT+5"), + ("2026-10-04T08:00:00Z", "Etc/GMT+8"), + ] + + def test_per_offering_cutovers_and_gaps(self) -> None: + local_hourly = offering("local-hourly", duration="PT1H", sla="PT1H") + config = subscription_config(dimensions=["package"], match="all") + config["selector"]["period_duration"] = "P1D" + daily_buy = buy(end_time="2026-10-05T04:00:00Z", offering_ids=("daily-pkg",)) + gaps = [ + { + "from": "2026-10-02T04:00:00Z", + "until": "2026-10-03T04:00:00Z", + "reason": "unauthorized", + "offering_id": "some-other-offering", + } + ] + # A gap naming another offering leaves this one's periods owed. + assert starts( + subscription=state("2026-10-01T00:00:00Z", config=config), + offerings=[DAILY, local_hourly], + buys=[daily_buy], + gaps=gaps, + ) == days("2026-10-01", 4) + gaps[0]["offering_id"] = "daily-pkg" + assert starts( + subscription=state("2026-10-01T00:00:00Z", config=config), + offerings=[DAILY], + buys=[daily_buy], + gaps=gaps, + ) == ["2026-10-01T04:00:00Z", *days("2026-10-03", 2)] + # A timezone cutover naming another offering leaves this one's zone. + moved = run( + offerings=[DAILY], + buys=[daily_buy], + timezone_changes=[ + AccountTimezoneChange( + at("2026-10-02T04:00:00Z"), + "America/Los_Angeles", + offering_id="other", + previous_timezone=NY, + ) + ], + ) + assert {p.source_timezone for p in moved} == {NY} + + def test_period_timezone_matching_is_as_specified(self) -> None: + utc_daily = offering("utc-daily", alignment="utc") + resolved_source = offering( + "src-resolved", + alignment="source_timezone", + period_timezone="Asia/Tokyo", + period_timezone_policy="account_resolved", + ) + + def override(zone: str) -> dict: + return subscription_config( + dimensions=["package"], + match="all", + scope={"account_ids": ["acct-1"]}, + period_timezone=zone, + ) + + product = ("utc-daily", "src-resolved") + offers = [utc_daily, resolved_source] + # Only UTC and Etc/UTC are UTC aliases; an account-resolved source + # zone is not a published zone, so it never matches an override. + assert resolved(override("UTC"), offerings=offers, product=product) == ("utc-daily",) + assert resolved(override("Etc/Zulu"), offerings=offers, product=product) == () + assert resolved(override("Asia/Tokyo"), offerings=offers, product=product) == () + + def test_wire_offering_dimensions_drive_matching(self) -> None: + wire = { + "offering_id": "gam-daily", + "feed_purpose": "analytics", + "report_definition_id": "gam-delivery-v1", + "reporting_profile": {"id": "delivery"}, + "schedule": {"period_duration": "P1D", "alignment": "utc", "delivery_sla": "PT6H"}, + "supported_finality": ["official"], + "reconciliation_mode": "delivery_only", + "standard_dimensions": ["package"], + "unmapped_dimensions": ["device"], + } + projected = SubscriptionOffering.from_offering(wire) + assert projected.dimensions == frozenset({"package"}) + assert projected.unmapped_dimensions == frozenset({"device"}) + exact = subscription_config(dimensions=["package"]) + finest = subscription_config(dimensions=["package"], match="finest") + assert resolved(exact, offerings=[projected], product=("gam-daily",)) == () + assert resolved(finest, offerings=[projected], product=("gam-daily",)) == ("gam-daily",) + + +def test_timezone_change_requires_previous_timezone() -> None: + with pytest.raises(ValueError, match="previous_timezone"): + run( + buys=[buy()], + timezone_changes=[AccountTimezoneChange(at("2026-10-03T12:00:00Z"), "Etc/GMT+8")], + ) + + +def test_wire_offering_without_standard_dimensions_is_refused() -> None: + wire = { + "offering_id": "legacy", + "feed_purpose": "analytics", + "report_definition_id": "legacy-v1", + "reporting_profile": {"id": "delivery"}, + "schedule": {"period_duration": "P1D", "alignment": "utc", "delivery_sla": "PT6H"}, + "supported_finality": ["official"], + "reconciliation_mode": "delivery_only", + } + with pytest.raises(ValueError, match="standard_dimensions"): + SubscriptionOffering.from_offering(wire) + assert SubscriptionOffering.from_offering(wire, dimensions=()).dimensions == frozenset() + + +# -- expert review fixes --------------------------------------------------------- + + +class TestReviewFixes: + def test_buy_canceled_before_its_flight_owes_nothing(self) -> None: + monthly = offering("monthly-utc", duration="P1M", alignment="utc") + canceled = buy( + confirmed_at="2026-10-01T00:00:00Z", + start_time="2026-11-15T00:00:00Z", + end_time="2026-12-15T00:00:00Z", + canceled_at="2026-11-10T00:00:00Z", + offering_ids=("monthly-utc",), + ) + assert ( + run( + subscription=state( + "2026-10-01T00:00:00Z", config=subscription_config(duration="P1M") + ), + offerings=[monthly], + buys=[canceled], + now=at("2027-01-01T00:00:00Z"), + ) + == [] + ) + # P1D: starts 06:00 local and is canceled at 02:00 local the same day. + same_day = buy( + start_time="2026-10-05T10:00:00Z", + end_time="2026-10-09T04:00:00Z", + canceled_at="2026-10-05T06:00:00Z", + ) + assert run(buys=[same_day]) == [] + # A zero-length flight overlaps nothing either. + empty = buy(start_time="2026-10-05T10:00:00Z", end_time="2026-10-05T10:00:00Z") + assert run(buys=[empty]) == [] + + def test_two_cutovers_inside_one_period_never_overlap(self) -> None: + # The account moves LA -> NY at 10-03T12Z (E = 10-04T07Z) and a new + # version is accepted at 10-03T13Z, inside the same outgoing LA day. + subscription = state( + "2026-10-03T13:00:00Z", + version=2, + config=subscription_config(version=2), + prior=[(1, "2026-10-01T00:00:00Z", "2026-10-03T13:00:00Z")], + ) + kwargs = dict( + subscription=subscription, + account_timezone=NY, + buys=[buy(start_time="2026-10-02T07:00:00Z", end_time="2026-10-07T04:00:00Z")], + ) + at_change = run( + timezone_changes=[ + AccountTimezoneChange( + at("2026-10-03T12:00:00Z"), NY, previous_timezone="America/Los_Angeles" + ) + ], + **kwargs, + ) + at_effective = run( + timezone_changes=[ + AccountTimezoneChange( + at("2026-10-04T07:00:00Z"), NY, previous_timezone="America/Los_Angeles" + ) + ], + **kwargs, + ) + assert at_change == at_effective + spans = [(at(p.period_start), at(p.period_end)) for p in at_change] + assert all(end <= start for (_, end), (start, _) in zip(spans, spans[1:])) + assert [(p.period_start, p.delivery_config_version) for p in at_change] == [ + ("2026-10-02T07:00:00Z", 1), + ("2026-10-03T07:00:00Z", 1), # the LA day containing both changes + ("2026-10-05T04:00:00Z", 2), # first NY day starting at or after 10-04T07Z + ("2026-10-06T04:00:00Z", 2), + ] + + def test_two_version_bumps_inside_one_period(self) -> None: + subscription = state( + "2026-10-03T15:00:00Z", + version=3, + config=subscription_config(version=3), + prior=[ + (2, "2026-10-03T14:00:00Z", "2026-10-03T15:00:00Z"), + (1, "2026-10-01T00:00:00Z", "2026-10-03T14:00:00Z"), + ], + ) + result = run( + subscription=subscription, + version_configurations={2: subscription_config(version=2, duration="PT1H")}, + buys=[buy(end_time="2026-10-06T04:00:00Z", offering_ids=("daily-pkg", "hourly-pkg"))], + ) + spans = [(at(p.period_start), at(p.period_end)) for p in result] + assert all(end <= start for (_, end), (start, _) in zip(spans, spans[1:])) + assert [(p.period_start, p.delivery_config_version) for p in result][2:] == [ + ("2026-10-03T04:00:00Z", 1), + ("2026-10-04T04:00:00Z", 3), + ("2026-10-05T04:00:00Z", 3), + ] + + def test_unknown_dimension_values_parse_and_match_nothing(self) -> None: + from adcp.types import SyncPrincipalRequest # noqa: F401 (applies the preview) + from adcp.types.domains.core.reporting_subscription import ReportingSubscription + + config = ReportingSubscription.model_validate(subscription_config(dimensions=["device"])) + assert [str(item) for item in config.selector.dimensions] == ["device"] + assert resolved(subscription_config(dimensions=["device"])) == () + wire = { + "offering_id": "pkg-device", + "feed_purpose": "analytics", + "report_definition_id": "d", + "reporting_profile": {"id": "delivery"}, + "schedule": {"period_duration": "P1D", "alignment": "utc", "delivery_sla": "PT6H"}, + "supported_finality": ["official"], + "reconciliation_mode": "delivery_only", + "standard_dimensions": ["package", "device"], + } + projected = SubscriptionOffering.from_offering(wire) + assert projected.dimensions == frozenset({"package", "device"}) + device = subscription_config(dimensions=["device"]) + assert resolved(device, offerings=[projected], product=("pkg-device",)) == () + finest = subscription_config(dimensions=["device"], match="finest") + assert resolved(finest, offerings=[projected], product=("pkg-device",)) == ("pkg-device",) + + def test_offerings_with_month_slas_are_refused_at_construction(self) -> None: + with pytest.raises(ValueError, match="delivery_sla"): + offering("monthly-sla", duration="P1M", sla="P1M") + + def test_non_iana_zone_keys_are_refused(self) -> None: + for zone in ("posixrules", "right/UTC", "posix/America/New_York", "+05:00"): + with pytest.raises(ValueError): + run(account_timezone=zone, buys=[buy()]) + + def test_prior_versions_carry_their_configuration(self) -> None: + readback = state( + "2026-10-03T12:00:00Z", + version=2, + config=subscription_config(version=2, scope={"account_ids": ["acct-2"]}), + prior=[(1, "2026-10-01T00:00:00Z", "2026-10-03T12:00:00Z")], + ) + readback["prior_versions"][0]["configuration"] = subscription_config() + model = ReportingSubscriptionState.model_validate(readback) + # v1 covered acct-1; the readback says so without buyer records. + assert starts(subscription=model, buys=[buy()]) == days("2026-10-01", 3) + bare = state( + "2026-10-03T12:00:00Z", + version=2, + prior=[(1, "2026-10-01T00:00:00Z", "2026-10-03T12:00:00Z")], + ) + with pytest.raises(ValueError, match="prior subscription_version 1"): + subscription_version_windows(bare, require_prior_configurations=True) + + def test_version_accepted_while_inactive_has_an_empty_window(self) -> None: + readback = state( + "2026-10-05T00:00:00Z", + version=2, + config=subscription_config(version=2), + prior=[(1, "2026-10-01T00:00:00Z", "2026-10-05T00:00:00Z")], + deactivated_at="2026-10-03T12:00:00Z", + ) + # The readback carries the still-open interval from v1's deactivation. + readback["inactive_intervals"] = [{"deactivated_at": "2026-10-03T12:00:00Z"}] + assert starts(subscription=readback, buys=[buy()]) == days("2026-10-01", 3) + + +class TestSubDailyLocalPeriods: + """adcp#8588: sub-daily local periods restart at each local midnight.""" + + def hours(self, zone: str, start: str, until: str, duration: str = "PT1H") -> list[str]: + local = offering("local", duration=duration, sla="PT1H") + return [ + p.period_start + for p in run( + subscription=state( + "2026-01-01T00:00:00Z", config=subscription_config(duration=duration) + ), + offerings=[local], + account_timezone=zone, + buys=[ + buy( + confirmed_at="2026-01-01T00:00:00Z", + start_time=start, + end_time=until, + offering_ids=("local",), + ) + ], + now=at("2027-01-01T00:00:00Z"), + ) + ] + + def test_hours_start_on_the_local_hour_whatever_the_1970_offset(self) -> None: + # Asia/Singapore was UTC+07:30 in 1970; its hours still start on :00. + result = self.hours("Asia/Singapore", "2026-10-01T00:00:00Z", "2026-10-01T03:00:00Z") + assert result == ["2026-10-01T00:00:00Z", "2026-10-01T01:00:00Z", "2026-10-01T02:00:00Z"] + kathmandu = self.hours("Asia/Kathmandu", "2026-10-01T00:15:00Z", "2026-10-01T02:15:00Z") + assert kathmandu == ["2026-10-01T00:15:00Z", "2026-10-01T01:15:00Z"] + + def test_dst_days_have_23_and_25_hours(self) -> None: + spring = self.hours(NY, "2026-03-08T05:00:00Z", "2026-03-09T04:00:00Z") + fall = self.hours(NY, "2026-11-01T04:00:00Z", "2026-11-02T05:00:00Z") + assert (len(spring), len(fall)) == (23, 25) + # Both 01:00 wall times are boundaries on the fall-back day. + assert fall[1:3] == ["2026-11-01T05:00:00Z", "2026-11-01T06:00:00Z"] + + def test_six_hour_periods_do_not_drift_with_dst(self) -> None: + summer = self.hours(NY, "2026-07-01T04:00:00Z", "2026-07-02T04:00:00Z", "PT6H") + winter = self.hours(NY, "2026-01-05T05:00:00Z", "2026-01-06T05:00:00Z", "PT6H") + assert summer == [ + "2026-07-01T04:00:00Z", + "2026-07-01T10:00:00Z", + "2026-07-01T16:00:00Z", + "2026-07-01T22:00:00Z", + ] + assert winter == [ + "2026-01-05T05:00:00Z", + "2026-01-05T11:00:00Z", + "2026-01-05T17:00:00Z", + "2026-01-05T23:00:00Z", + ] + + def test_local_durations_must_divide_a_day_and_not_mix_components(self) -> None: + for duration in ("PT7H", "P1DT1H"): + with pytest.raises(ValueError): + self.hours(NY, "2026-07-01T04:00:00Z", "2026-07-02T04:00:00Z", duration) + + def test_utc_alignment_is_unchanged(self) -> None: + result = periods( + subscription=state("2026-10-03T00:00:00Z", config=subscription_config(duration="PT1H")), + buys=[ + buy( + start_time="2026-10-03T10:00:00Z", + end_time="2026-10-03T12:00:00Z", + offering_ids=("hourly-pkg",), + ) + ], + ) + assert result == [ + ("2026-10-03T10:00:00Z", "2026-10-03T11:00:00Z"), + ("2026-10-03T11:00:00Z", "2026-10-03T12:00:00Z"), + ] + + +def test_a_bare_3_3_version_cannot_be_served_while_beta_1_is_pinned() -> None: + # The gate would serve 3.3 the adcp#8585 fields, but the dispatcher never + # resolves a bare "3.3" while only the 3.3.0-beta.1 bundle is pinned. + from adcp._version import get_supported_adcp_versions, resolve_adcp_version + from adcp.exceptions import ConfigurationError + + with pytest.raises(ConfigurationError): + resolve_adcp_version("3.3") + assert "3.3" not in get_supported_adcp_versions() diff --git a/tests/test_reporting_subscription_parity.py b/tests/test_reporting_subscription_parity.py new file mode 100644 index 000000000..f422dc208 --- /dev/null +++ b/tests/test_reporting_subscription_parity.py @@ -0,0 +1,233 @@ +"""Cross-SDK parity: the TypeScript SDK's coverage table, run against Python. + +``tests/fixtures/reporting_subscriptions/coverage-cases.json`` is copied from +the TypeScript SDK (its ``$source`` names the commit). Each case gives plain +input and the exact owed ``(subscription_version, media_buy_id, offering_id, +period)`` rows, derived by hand from the spec rule, or an expected error. Both +SDKs must produce identical periods, so this test maps the shared input onto +:func:`expected_subscription_periods` and compares instants row by row. +""" + +from __future__ import annotations + +import json +from datetime import datetime +from pathlib import Path +from typing import Any + +import pytest + +from adcp.reporting.subscriptions import ( + SubscriptionAuthorization, + SubscriptionBuy, + SubscriptionBuyFlight, + SubscriptionBuyPackage, + SubscriptionDestination, + SubscriptionOffering, + SubscriptionVersionWindow, + expected_subscription_periods, +) + +FIXTURE = Path(__file__).parent / "fixtures" / "reporting_subscriptions" / "coverage-cases.json" +CASES = json.loads(FIXTURE.read_text())["cases"] + + +def at(value: str | None) -> datetime | None: + return None if value is None else datetime.fromisoformat(value.replace("Z", "+00:00")) + + +def windows( + versions: list[dict[str, Any]], readback_intervals: list[dict[str, Any]] = () +) -> list[SubscriptionVersionWindow]: + # The readback's inactive_intervals (adcp#8585) apply to every version. + shared = [ + (at(item["deactivated_at"]), at(item.get("reactivated_at"))) for item in readback_intervals + ] + result = [] + for index, record in enumerate(versions): + following = versions[index + 1] if index + 1 < len(versions) else None + ends_at = at(record.get("superseded_at")) or ( + at(following["accepted_at"]) if following else None + ) + inactive = [ + (at(item["from"]), at(item.get("until"))) + for item in record.get("inactive_intervals", ()) + ] + inactive.extend(shared) + if "deactivated_at" in record: + inactive.append((at(record["deactivated_at"]), None)) + method = record.get("destination_method") + result.append( + SubscriptionVersionWindow( + subscription_version=record["subscription_version"], + accepted_at=at(record["accepted_at"]), + ends_at=ends_at, + configuration=record["configuration"], + inactive_intervals=tuple(inactive), + destination=( + None + if method is None + else SubscriptionDestination(method["pattern"], method.get("transport")) + ), + ) + ) + return result + + +def offerings(input_: dict[str, Any]) -> list[SubscriptionOffering]: + source_timezone = input_["account"].get("source_timezone") + result = [] + for wire in input_["offerings"]: + schedule = wire["schedule"] + resolved = ( + source_timezone + if schedule["alignment"] == "source_timezone" + and schedule.get("period_timezone_policy") != "fixed" + else None + ) + result.append( + SubscriptionOffering.from_offering( + wire, + period_timezone=resolved, + available_from=at(wire.get("available_from")), + withdrawn_at=at(wire.get("withdrawn_at")), + ) + ) + return result + + +def buys(input_: dict[str, Any]) -> list[SubscriptionBuy]: + result = [] + for buy in input_["buys"]: + result.append( + SubscriptionBuy( + media_buy_id=buy["media_buy_id"], + confirmed_at=at(buy.get("confirmed_at")), + start_time=at(buy["start_time"]), + end_time=at(buy["end_time"]), + canceled_at=at(buy.get("canceled_at")), + completed_at=at(buy.get("completed_at")), + packages=tuple( + SubscriptionBuyPackage( + item["package_id"], tuple(item["reporting_delivery_offering_ids"]) + ) + for item in buy["packages"] + ), + superseded_flights=tuple( + SubscriptionBuyFlight( + superseded_at=at(flight["replaced_at"]), + start_time=at(flight["start_time"]), + end_time=at(flight["end_time"]), + canceled_at=at(flight.get("canceled_at")), + completed_at=at(flight.get("completed_at")), + ) + for flight in buy.get("prior_flights", ()) + ), + ) + ) + return result + + +def periods(input_: dict[str, Any]): + account = input_["account"] + return expected_subscription_periods( + subscription=windows(input_["versions"], input_.get("inactive_intervals", [])), + account_id=account["account_id"], + buys=buys(input_), + offerings=offerings(input_), + account_timezone=account.get("timezone"), + account_operator=account.get("operator"), + now=at(input_["now"]), + authorizations=[ + SubscriptionAuthorization(at(grant["authorized_at"]), at(grant.get("unauthorized_at"))) + for grant in account.get("grants", ()) + ], + coverage=input_.get("coverage"), + backfill_on_regrant=bool(input_.get("backfill_on_regrant", False)), + status_retention_days=input_.get("status_retention_days"), + ) + + +def rows(owed) -> list[tuple]: + """Per-buy rows in the table's order: start, version, offering, buy (byte order).""" + return [ + ( + at(p.period_start), + p.delivery_config_version, + p.offering_id.encode(), + media_buy_id.encode(), + at(p.period_end), + ) + for p in owed + for media_buy_id in p.media_buy_ids + ] + + +#: The table names JavaScript error classes. Python raises ValueError for the +#: malformed input TypeScript reports as TypeError (and RangeError). +ERROR_CLASSES = {"TypeError": ValueError, "RangeError": ValueError} + + +@pytest.mark.parametrize("case", CASES, ids=[case["name"] for case in CASES]) +def test_typescript_parity(case: dict[str, Any]) -> None: + if "expected_error" in case: + expected_class = ERROR_CLASSES[case["expected_error"]] + with pytest.raises(Exception) as raised: + periods(case["input"]) + # Strict: malformed input, not an incidental KeyError or AttributeError. + assert isinstance(raised.value, expected_class), (case["name"], raised.value) + return + owed = periods(case["input"]) + assert {p.delivery_config_id for p in owed} <= {case["input"]["subscription_id"]} + expected = [ + ( + at(row["period_start"]), + row["subscription_version"], + row["offering_id"].encode(), + row["media_buy_id"].encode(), + at(row["period_end"]), + ) + for row in case["expected"] + ] + # Strict: the same rows in the same order, not just the same set. + assert rows(owed) == expected, case.get("rule") + if "expected_obligations" in case: + obligations = [ + ( + p.delivery_config_version, + p.offering_id, + at(p.period_start), + at(p.period_end), + list(p.media_buy_ids), + ) + for p in owed + ] + assert obligations == [ + ( + item["subscription_version"], + item["offering_id"], + at(item["period_start"]), + at(item["period_end"]), + item["media_buy_ids"], + ) + for item in case["expected_obligations"] + ], case.get("rule") + + +def test_every_subscription_in_the_table_is_schema_valid() -> None: + """The table's subscriptions validate against core/reporting-subscription.json.""" + from adcp.validation import schema_loader + + validator = schema_loader.get_named_validator( + "core/reporting-subscription.json", version="3.3.0-beta.1" + ) + for case in CASES: + for version in case["input"]["versions"]: + errors = [ + error.message + for error in validator.iter_errors(version["configuration"]) + # The dimension vocabulary is x-extensible: receivers MUST + # accept values the beta.1 enum does not list. + if not (error.validator == "enum" and "dimensions" in list(error.absolute_path)) + ] + assert errors == [], (case["name"], version["subscription_version"], errors) diff --git a/tests/test_reporting_subscription_wire.py b/tests/test_reporting_subscription_wire.py new file mode 100644 index 000000000..b7a7f485c --- /dev/null +++ b/tests/test_reporting_subscription_wire.py @@ -0,0 +1,127 @@ +"""Version-gated projection of the adcp#8585 reporting-subscription fields.""" + +from __future__ import annotations + +import pytest + +from adcp.reporting.subscriptions import ( + project_reporting_delivery, + project_subscription_coverage, + project_subscription_state, + serves_settled_subscription_fields, +) + + +@pytest.mark.parametrize( + "version, served", + [ + (None, False), + ("3.2", False), + ("3.3-beta.1", False), + ("3.3.0-beta.1", False), + ("3.3-beta.2", True), + ("3.3.0-beta.10", True), + ("3.3-rc.1", True), + ("3.3", True), + ("3.4", True), + ("4.0", True), + ("not-a-version", False), + ], +) +def test_gate(version: str | None, served: bool) -> None: + assert serves_settled_subscription_fields(version) is served + + +def test_offering_dimensions_and_inactive_intervals_are_dropped_below_beta_2() -> None: + block = { + "offerings": [ + { + "offering_id": "a", + "standard_dimensions": ["package"], + "unmapped_dimensions": ["device"], + } + ] + } + assert project_reporting_delivery(block, "3.3-beta.1") == {"offerings": [{"offering_id": "a"}]} + assert project_reporting_delivery(block, "3.3-beta.2") == block + state = {"subscription_id": "s", "inactive_intervals": [{"deactivated_at": "x"}]} + assert "inactive_intervals" not in project_subscription_state(state, "3.3-beta.1") + assert project_subscription_state(state, "3.3-beta.2") == state + + +def test_per_offering_entries_become_account_wide_below_beta_2() -> None: + coverage = { + "subscription_id": "s", + "gaps": [ + { + "offering_id": "daily", + "from": "2026-10-03T04:00:00Z", + "until": "2026-10-05T04:00:00Z", + "reason": "unauthorized", + }, + { + "offering_id": "utc", + "from": "2026-10-03T00:00:00Z", + "until": "2026-10-05T00:00:00Z", + "reason": "unauthorized", + }, + ], + "timezone_cutovers": [ + { + "offering_id": "daily", + "effective_at": "2026-10-04T04:00:00Z", + "timezone": "America/Los_Angeles", + "previous_timezone": "America/New_York", + }, + { + "offering_id": "hourly", + "effective_at": "2026-10-03T13:00:00Z", + "timezone": "America/Los_Angeles", + "previous_timezone": "America/New_York", + }, + ], + } + projected = project_subscription_coverage(coverage, "3.3-beta.1") + # One account-wide gap spanning both offerings' gaps. + assert projected["gaps"] == [ + {"from": "2026-10-03T00:00:00Z", "until": "2026-10-05T04:00:00Z", "reason": "unauthorized"} + ] + # One cutover per change, at the latest effective_at, without the new fields. + assert projected["timezone_cutovers"] == [ + {"effective_at": "2026-10-04T04:00:00Z", "timezone": "America/Los_Angeles"} + ] + assert project_subscription_coverage(coverage, "3.3-beta.2") == coverage + + +def test_distinct_changes_back_to_a_zone_stay_distinct_below_beta_2() -> None: + cutovers = [ + { + "effective_at": "2026-01-10T05:00:00Z", + "timezone": "America/Los_Angeles", + "previous_timezone": "America/New_York", + }, + { + "effective_at": "2026-03-10T08:00:00Z", + "timezone": "America/New_York", + "previous_timezone": "America/Los_Angeles", + }, + { + "effective_at": "2026-06-10T04:00:00Z", + "timezone": "America/Los_Angeles", + "previous_timezone": "America/New_York", + }, + ] + projected = project_subscription_coverage({"timezone_cutovers": cutovers}, "3.3-beta.1") + assert [item["effective_at"] for item in projected["timezone_cutovers"]] == [ + "2026-01-10T05:00:00Z", + "2026-03-10T08:00:00Z", + "2026-06-10T04:00:00Z", + ] + + +def test_prior_version_configuration_is_dropped_below_beta_2() -> None: + state = {"prior_versions": [{"subscription_version": 1, "configuration": {"x": 1}}]} + assert project_subscription_state(state, "3.3-beta.1")["prior_versions"] == [ + {"subscription_version": 1} + ] + assert project_subscription_state(state, "3.3-beta.2") == state