diff --git a/docs/reporting-subscriptions-errors.md b/docs/reporting-subscriptions-errors.md index 652a6ccb7..2c6e7dfff 100644 --- a/docs/reporting-subscriptions-errors.md +++ b/docs/reporting-subscriptions-errors.md @@ -33,3 +33,32 @@ Every SDK error carries `code` and `recovery` (`transient`, `correctable` or `te | `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. + +## 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 | + +When the seller does not record submitted receipts, `reconcile` raises `ReportingReconciliationError` with `code` `RECEIPT_WRITE_FAILED` (it carries no `recovery`); 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. + +## 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 b4cacf5a1..46552852d 100644 --- a/docs/reporting-subscriptions-migration.md +++ b/docs/reporting-subscriptions-migration.md @@ -1,13 +1,33 @@ # 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. +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. +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.