From a48b86840a005ebf001761d659f2ad8b557bfcc8 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 1 Nov 2026 18:23:28 +0000 Subject: [PATCH 1/2] docs(reporting): write the migration guide and extend the error reference The migration guide walks the handover from account-level reporting_delivery_configs to a subscription: subscribe scoped to the accounts already served, read the overridden periods with reconcile, hand over with replace_account_configs, then widen the scope. It keeps the Precedence, Overrides and replace_account_configs anchors. The error reference adds the reconcile issue codes, the events and receipts failures, the account change refusals and the coverage trace input errors, beside the frozen subscription codes it already listed. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/reporting-subscriptions-errors.md | 40 +++++++++++++++++++++++ docs/reporting-subscriptions-migration.md | 25 ++++++++++++-- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/docs/reporting-subscriptions-errors.md b/docs/reporting-subscriptions-errors.md index 8bd7716ed5..79a91fbe16 100644 --- a/docs/reporting-subscriptions-errors.md +++ b/docs/reporting-subscriptions-errors.md @@ -21,3 +21,43 @@ Every SDK error carries `code` and `recovery` (`transient`, `correctable` or `te | `SUBSCRIPTION_ROSTER_UNAVAILABLE` | `ReliableReportingConfigurationError` | n/a | correctable | serve `list_accounts`, or supply `SubscriptionOptions.accounts` | `AccountRequestError` is raised only when `client.accounts.ensure` could not be processed; it carries the wire codes. Account outcomes are values. + +## Reconcile issues + +`client.reporting.reconcile()` and `poll()` never return a smaller success. When a read fails, `report.definitive` is `False` and `report.issues` carries one `ReconcileIssue(code, message, account_id, subscription_id)` per problem. The message ends with what to retry. These are values, not exceptions. + +| Code | Cause | Fix | +|---|---|---| +| `SUBSCRIPTION_PRINCIPAL_UNAVAILABLE` | `get_principal` failed, so nothing could be computed | retry once the seller answers (`explain()` raises `ReportingSubscriptionError` instead) | +| `SUBSCRIPTION_OFFERINGS_UNAVAILABLE` | capabilities could not be read | retry; no offering can resolve until it succeeds | +| `SUBSCRIPTION_ROSTER_UNAVAILABLE` | `list_accounts` failed or did not finish paging, or `accounts_unauthorized` lists more than its inline page | retry; a partial roster is never used | +| `SUBSCRIPTION_ACCOUNT_UNREADABLE` | `get_media_buys` or `get_reporting_status` failed for one account | retry; the account is reported from the last facts read | +| `SUBSCRIPTION_LEDGER_UNAVAILABLE` | the account's obligations walk did not complete | retry; the periods are listed without delivery status | +| `SUBSCRIPTION_ACCOUNT_NOT_COVERED` | an explicit `accounts=` id is not readable, or no subscription covers it | check `list_accounts` and the subscription scopes | +| `COVERAGE_INPUT_INCOMPLETE`, `COVERAGE_INPUT_INVALID` | an owed set could not be computed, for example a missing account zone | the `detail` names the input | + +## Events and receipts + +| Code | Class | `dispatched` | `recovery` | Fix | +|---|---|---|---|---| +| `REPORTING_EVENTS_CONFLICT` | `ReportingEventConflictError` | absent | transient | another worker acknowledged this snapshot first; poll again | +| `RECEIPT_WRITE_FAILED` | `ReportingReconciliationError` | absent | transient | the seller did not record the receipts; reconcile again | + +A revision the receipts policy could not verify is listed in `report.receipts.failed` as `(reporting_obligation_id, code)`. The code is the inspector's, for example `MANIFEST_DIGEST_MISMATCH`, or `INSPECTION_FAILED`. Nothing is receipted for it. + +## Accounts + +`client.accounts.change()` refuses locally, before anything is sent, with `AccountRequestError` (`dispatched=False`, `recovery="correctable"`): + +| Code | Cause | Fix | +|---|---|---| +| `ACCOUNT_REVISION_REQUIRED` | a change to `billing`, `billing_entity` or `payment_terms` without `revision` | pass the revision the change was approved against, read from `list_accounts` | +| `ACCOUNT_CHANGE_INVALID` | no attribute, or one `change()` cannot change | change `brand`, `operator_unit`, `currency`, `timezone` or a money term | +| `ACCOUNT_REQUESTS_UNSUPPORTED` | the seller does not advertise `account.requests` | change the account through `sync_accounts` | + +A seller rejection is a value: `AccountRejected`. Its `requires_new_account` is `True` for `ACCOUNT_CHANGE_REQUIRES_NEW_ACCOUNT`; resend with `allow_separate_account=True`. A `CONFLICT` on a money change is raised as `AccountRequestError`, because the human must approve the change against the new revision. + +## Coverage traces + +`explain()` and `explain_coverage()` raise `CoverageInputError` (`COVERAGE_INPUT_INVALID`, detail `invalid_input`) when `period` is not a date, an instant or `{start, end}`, or when `{start, end}` is not a period of the offering's schedule. They raise `KeyError` for an unknown `subscription_id`. A trace's `reason` is always a code from the spec's `reason-codes.json`; see [Coverage, explained](reporting-subscriptions-coverage.md#reason-codes). + diff --git a/docs/reporting-subscriptions-migration.md b/docs/reporting-subscriptions-migration.md index b4cacf5a1b..20d149098e 100644 --- a/docs/reporting-subscriptions-migration.md +++ b/docs/reporting-subscriptions-migration.md @@ -1,13 +1,32 @@ # Migrating from account-level `reporting_delivery_configs` +A subscription registers reporting once on the principal, so it follows the caller's grants across every account. Account-level `reporting_delivery_configs`, written through `sync_accounts`, register reporting once per account. The two coexist: during a migration, both can be live on the same account. + +```python +# 1. Subscribe, scoped to the accounts already served at account level. +await client.reporting.subscribe({ + "subscription_id": "analytics-daily", # never equal to a delivery_config_id + "scope": {"account_ids": ["acc_a", "acc_b"]}, + "selector": {"feed_purpose": "analytics", "period_duration": "P1D", "required_finality": "official"}, + "reconciliation_mode": "delivery_only", +}) +# 2. Check what the subscription would own: your account-level configs still take precedence. +report = await client.reporting.reconcile(include_not_owed=True) # overridden periods read excluded:overridden +# 3. Hand over: end the account-level configs where the subscription resolves an offering. +await client.reporting.subscribe({**subscription, "replace_account_configs": True}) +# 4. Widen the scope once every account is handed over. +await client.reporting.subscribe({**subscription, "scope": {"all_authorized_accounts": True}, + "operator_domains": ["agency.example"]}) +``` + ## Precedence -A caller's own active `sync_accounts` `reporting_delivery_configs` entry for a `feed_purpose` takes precedence over its subscriptions on that account. The subscription records an `overridden` exception, and the two never owe the same period. On the managed path, supply `SubscriptionOptions.account_delivery_configs` so the engine sees those configurations. +A caller's own active `sync_accounts` `reporting_delivery_configs` entry for a `feed_purpose` takes precedence over its subscriptions on that account. The subscription records an `overridden` exception, and the two never owe the same period: `reconcile()` and `explain()` read those periods as `excluded:overridden`. On the managed path, supply `SubscriptionOptions.account_delivery_configs` so the engine sees those configurations. ## Overrides -Give each configuration its `active_from`, so the exception opens at that instant. A `subscription_id` never equals a `delivery_config_id` the caller uses: they share one namespace. `reporting.principals.reporting_subscription_ids(...)` lets `sync_accounts` refuse a clash. +Give each configuration its `active_from`, so the exception opens at that instant. A `subscription_id` never equals a `delivery_config_id` the caller uses: they share one namespace. `client.reporting.subscribe` refuses a clash before dispatch (`SUBSCRIPTION_ID_CLASH`). On the seller side, `reporting.principals.reporting_subscription_ids(...)` lets `sync_accounts` refuse the reverse clash. ## `replace_account_configs` -A subscription with `replace_account_configs: true` ends the caller's own account-level configurations with the same `feed_purpose` on in-scope accounts where it resolves an offering. It applies once per `subscription_version`, or when the flag changes to true. Supply `SubscriptionOptions.end_account_configs(principal, account_id, ids, *, at)` together with `account_delivery_configs`; without them the flag is refused with `UNSUPPORTED_FEATURE`. +A subscription with `replace_account_configs: true` ends the caller's own account-level configurations with the same `feed_purpose` on in-scope accounts where it resolves an offering. It applies once per `subscription_version`, or when the flag changes to true; resending the same version does nothing more. Supply `SubscriptionOptions.end_account_configs(principal, account_id, ids, *, at)` together with `account_delivery_configs`; without them the flag is refused with `UNSUPPORTED_FEATURE`. Periods that started before the handover stay owed by the account-level configuration, and the subscription owes the rest: run `kit.assert_agreement` in the [test kit](reporting-subscriptions-testing.md#testing-today) across a handover before you ship it. From 9465e85b62f8d2e61f8e67aa90650b41eaa3266b Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 1 Nov 2026 18:41:48 +0000 Subject: [PATCH 2/2] docs(reporting): bind the migration example and scope the error reference The migration example binds subscription before reusing it. The account change refusals move to the accounts slice, which ships them, and the receipt write failure no longer claims a recovery its class does not carry. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/reporting-subscriptions-errors.md | 15 ++------------- docs/reporting-subscriptions-migration.md | 5 +++-- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/docs/reporting-subscriptions-errors.md b/docs/reporting-subscriptions-errors.md index 79a91fbe16..805de8dde3 100644 --- a/docs/reporting-subscriptions-errors.md +++ b/docs/reporting-subscriptions-errors.md @@ -41,21 +41,10 @@ Every SDK error carries `code` and `recovery` (`transient`, `correctable` or `te | Code | Class | `dispatched` | `recovery` | Fix | |---|---|---|---|---| | `REPORTING_EVENTS_CONFLICT` | `ReportingEventConflictError` | absent | transient | another worker acknowledged this snapshot first; poll again | -| `RECEIPT_WRITE_FAILED` | `ReportingReconciliationError` | absent | transient | the seller did not record the receipts; reconcile again | -A revision the receipts policy could not verify is listed in `report.receipts.failed` as `(reporting_obligation_id, code)`. The code is the inspector's, for example `MANIFEST_DIGEST_MISMATCH`, or `INSPECTION_FAILED`. Nothing is receipted for it. - -## Accounts +When the seller does not record submitted receipts, `reconcile` raises `ReportingReconciliationError` with `code` `RECEIPT_WRITE_FAILED` (it carries no `recovery`); reconcile again. -`client.accounts.change()` refuses locally, before anything is sent, with `AccountRequestError` (`dispatched=False`, `recovery="correctable"`): - -| Code | Cause | Fix | -|---|---|---| -| `ACCOUNT_REVISION_REQUIRED` | a change to `billing`, `billing_entity` or `payment_terms` without `revision` | pass the revision the change was approved against, read from `list_accounts` | -| `ACCOUNT_CHANGE_INVALID` | no attribute, or one `change()` cannot change | change `brand`, `operator_unit`, `currency`, `timezone` or a money term | -| `ACCOUNT_REQUESTS_UNSUPPORTED` | the seller does not advertise `account.requests` | change the account through `sync_accounts` | - -A seller rejection is a value: `AccountRejected`. Its `requires_new_account` is `True` for `ACCOUNT_CHANGE_REQUIRES_NEW_ACCOUNT`; resend with `allow_separate_account=True`. A `CONFLICT` on a money change is raised as `AccountRequestError`, because the human must approve the change against the new revision. +A revision the receipts policy could not verify is listed in `report.receipts.failed` as `(reporting_obligation_id, code)`. The code is the inspector's, for example `MANIFEST_DIGEST_MISMATCH`, or `INSPECTION_FAILED`. Nothing is receipted for it. ## Coverage traces diff --git a/docs/reporting-subscriptions-migration.md b/docs/reporting-subscriptions-migration.md index 20d149098e..46552852d6 100644 --- a/docs/reporting-subscriptions-migration.md +++ b/docs/reporting-subscriptions-migration.md @@ -4,12 +4,13 @@ A subscription registers reporting once on the principal, so it follows the call ```python # 1. Subscribe, scoped to the accounts already served at account level. -await client.reporting.subscribe({ +subscription = { "subscription_id": "analytics-daily", # never equal to a delivery_config_id "scope": {"account_ids": ["acc_a", "acc_b"]}, "selector": {"feed_purpose": "analytics", "period_duration": "P1D", "required_finality": "official"}, "reconciliation_mode": "delivery_only", -}) +} +await client.reporting.subscribe(subscription) # 2. Check what the subscription would own: your account-level configs still take precedence. report = await client.reporting.reconcile(include_not_owed=True) # overridden periods read excluded:overridden # 3. Hand over: end the account-level configs where the subscription resolves an offering.