id: notification_config_lifecycle
version: "1.0.0"
title: "Notification config lifecycle"
category: account_lifecycle
summary: "Validates account-level notification_configs[] lifecycle behavior on sync_accounts: paused registration, durable echo, subscriber-keyed replacement, and clear."
track: core
required_tools:
  - sync_accounts
  - list_accounts

narrative: |
  AdCP 3.1 makes `sync_accounts.accounts[].notification_configs[]` the
  durable account-level subscription surface for creative lifecycle and
  wholesale-feed webhook events. This storyboard verifies the positive
  lifecycle: a paused subscriber can be registered without endpoint proof,
  the seller echoes the applied state on `sync_accounts` and `list_accounts`, re-sending the same
  `subscriber_id` replaces the subscriber rather than appending a duplicate,
  inactive entries are preserved as paused subscriptions, and `[]` clears the
  subscriber set. Active registration is intentionally out of scope here:
  sellers MUST complete the account-level proof-of-control challenge before
  persisting or echoing a new subscriber as `active: true`.

agent:
  interaction_model: media_buy_seller
  capabilities: []
  examples:
    - "Any AdCP agent that accepts sync_accounts and list_accounts"

caller:
  role: buyer_agent
  example: "Compliance test harness"

prerequisites:
  description: |
    The agent under test exposes `sync_accounts` and `list_accounts`. The
    sample requests use sandbox provisioning mode first, then settings-update
    mode by returned `account_id` so both sync_accounts key shapes are
    exercised without requiring out-of-band explicit account setup.
  test_kit: "test-kits/acme-outdoor.yaml"
  controller_seeding: false

phases:
  - id: register_and_echo_paused_subscriber
    title: "Register and read a paused account-level subscriber"
    narrative: |
      The buyer provisions a sandbox account with one paused creative lifecycle
      subscriber. Paused entries do not receive fires and may skip the outbound
      endpoint-control proof while inactive, but the seller must still persist
      and echo the applied state without relying on any media-buy-scoped webhook
      surface.

    steps:
      - id: sync_accounts_create_paused_notification_config
        title: "Create account with paused notification config"
        task: sync_accounts
        schema_ref: "account/sync-accounts-request.json"
        response_schema_ref: "account/sync-accounts-response.json"
        doc_ref: "/accounts/tasks/sync_accounts"
        stateful: true
        context_outputs:
          - name: notification_config_account_id
            path: "accounts[0].account_id"
        expected: |
          Create or update the sandbox account and echo one paused
          notification_configs[] entry keyed by subscriber_id.

        sample_request:
          accounts:
            - brand:
                domain: "acmeoutdoor.example"
              operator: "pinnacle-agency.example"
              billing: "operator"
              payment_terms: "net_60"
              sandbox: true
              notification_configs:
                - subscriber_id: "buyer-primary"
                  url: "https://example.com/webhooks/adcp/account-primary"
                  event_types:
                    - "creative.status_changed"
                    - "creative.purged"
                  active: false
          idempotency_key: "$generate:uuid_v4#notification_config_lifecycle_create_paused"
          context:
            correlation_id: "notification_config_lifecycle--create_paused"

        validations:
          - check: response_schema
            description: "Response matches sync-accounts-response.json schema"
          - check: field_present
            path: "accounts[0].account_id"
            description: "Account ID is returned for settings-update follow-up"
          - check: field_value
            path: "accounts[0].notification_configs[0].subscriber_id"
            value: "buyer-primary"
            description: "Subscriber key is echoed"
          - check: field_value
            path: "accounts[0].notification_configs[0].url"
            value: "https://example.com/webhooks/adcp/account-primary"
            description: "Subscriber endpoint is echoed"
          - check: field_value
            path: "accounts[0].notification_configs[0].event_types[0]"
            value: "creative.status_changed"
            description: "Creative status event is registered"
          - check: field_value
            path: "accounts[0].notification_configs[0].event_types[1]"
            value: "creative.purged"
            description: "Creative purge event is registered"
          - check: field_value
            path: "accounts[0].notification_configs[0].active"
            value: false
            description: "Paused subscriber is persisted without requiring activation proof"
          - check: field_absent
            path: "accounts[0].notification_configs[1].subscriber_id"
            description: "Exactly one subscriber is present"
          - check: field_value
            path: "context.correlation_id"
            value: "notification_config_lifecycle--create_paused"
            description: "Context correlation_id returned unchanged"

      - id: list_accounts_echoes_notification_config
        title: "Read persisted notification config"
        task: list_accounts
        schema_ref: "account/list-accounts-request.json"
        response_schema_ref: "account/list-accounts-response.json"
        doc_ref: "/accounts/tasks/list_accounts"
        stateful: true
        expected: |
          Return the sandbox account with the persisted notification config.

        sample_request:
          account:
            account_id: "$context.notification_config_account_id"
          status: "active"
          sandbox: true
          pagination:
            max_results: 100
          context:
            correlation_id: "notification_config_lifecycle--list_after_create"

        validations:
          - check: response_schema
            description: "Response matches list-accounts-response.json schema"
          - check: field_value
            path: "accounts[0].account_id"
            value: "$context.notification_config_account_id"
            description: "The configured account is visible on list_accounts"
          - check: field_value
            path: "accounts[0].notification_configs[0].subscriber_id"
            value: "buyer-primary"
            description: "Persisted subscriber is visible on list_accounts"
          - check: field_value
            path: "accounts[0].notification_configs[0].active"
            value: false
            description: "Persisted subscriber remains paused"
          - check: field_value
            path: "context.correlation_id"
            value: "notification_config_lifecycle--list_after_create"
            description: "Context correlation_id returned unchanged"

  - id: replace_pause_and_clear
    title: "Replace, pause, and clear subscribers"
    narrative: |
      The buyer targets the account by `account_id` and sends a full desired
      subscriber set. The existing `subscriber_id` must be replaced in place,
      not duplicated. An inactive subscriber must remain persisted as paused,
      and a later empty array must clear the account's subscriber set.

    steps:
      - id: sync_accounts_replace_and_pause_subscriber
        title: "Replace subscriber fields and pause delivery"
        task: sync_accounts
        schema_ref: "account/sync-accounts-request.json"
        response_schema_ref: "account/sync-accounts-response.json"
        doc_ref: "/accounts/tasks/sync_accounts"
        stateful: true
        expected: |
          Replace the buyer-primary subscriber with the new endpoint and
          event filter, preserving exactly one paused subscriber.

        sample_request:
          accounts:
            - account:
                account_id: "$context.notification_config_account_id"
              payment_terms: "net_45"
              notification_configs:
                - subscriber_id: "buyer-primary"
                  url: "https://example.com/webhooks/adcp/account-paused"
                  event_types:
                    - "creative.purged"
                  active: false
          idempotency_key: "$generate:uuid_v4#notification_config_lifecycle_replace_pause"
          context:
            correlation_id: "notification_config_lifecycle--replace_pause"

        validations:
          - check: response_schema
            description: "Response matches sync-accounts-response.json schema"
          - check: field_value
            path: "accounts[0].action"
            value: "updated"
            description: "Settings-update mode updates the existing account"
          - check: field_value
            path: "accounts[0].notification_configs[0].subscriber_id"
            value: "buyer-primary"
            description: "Subscriber key is preserved"
          - check: field_value
            path: "accounts[0].notification_configs[0].url"
            value: "https://example.com/webhooks/adcp/account-paused"
            description: "Subscriber endpoint is replaced"
          - check: field_value
            path: "accounts[0].notification_configs[0].event_types[0]"
            value: "creative.purged"
            description: "Event filter is replaced"
          - check: field_value
            path: "accounts[0].notification_configs[0].active"
            value: false
            description: "Inactive subscriber is persisted as paused"
          - check: field_absent
            path: "accounts[0].notification_configs[1].subscriber_id"
            description: "Replacement by subscriber_id did not create a duplicate"
          - check: field_value
            path: "context.correlation_id"
            value: "notification_config_lifecycle--replace_pause"
            description: "Context correlation_id returned unchanged"

      - id: list_accounts_echoes_paused_replacement
        title: "Read paused replacement"
        task: list_accounts
        schema_ref: "account/list-accounts-request.json"
        response_schema_ref: "account/list-accounts-response.json"
        doc_ref: "/accounts/tasks/list_accounts"
        stateful: true
        expected: |
          Return the account with exactly one paused buyer-primary subscriber.

        sample_request:
          account:
            account_id: "$context.notification_config_account_id"
          status: "active"
          sandbox: true
          pagination:
            max_results: 100
          context:
            correlation_id: "notification_config_lifecycle--list_after_pause"

        validations:
          - check: response_schema
            description: "Response matches list-accounts-response.json schema"
          - check: field_value
            path: "accounts[0].account_id"
            value: "$context.notification_config_account_id"
            description: "The configured account is visible on list_accounts"
          - check: field_value
            path: "accounts[0].notification_configs[0].subscriber_id"
            value: "buyer-primary"
            description: "Paused subscriber remains persisted"
          - check: field_value
            path: "accounts[0].notification_configs[0].active"
            value: false
            description: "Paused state is visible on list_accounts"
          - check: field_value
            path: "context.correlation_id"
            value: "notification_config_lifecycle--list_after_pause"
            description: "Context correlation_id returned unchanged"

      - id: sync_accounts_clear_subscribers
        title: "Clear all account-level subscribers"
        task: sync_accounts
        schema_ref: "account/sync-accounts-request.json"
        response_schema_ref: "account/sync-accounts-response.json"
        doc_ref: "/accounts/tasks/sync_accounts"
        stateful: true
        expected: |
          Sending notification_configs: [] clears the account-level subscriber
          set and echoes an empty array.

        sample_request:
          accounts:
            - account:
                account_id: "$context.notification_config_account_id"
              notification_configs: []
          idempotency_key: "$generate:uuid_v4#notification_config_lifecycle_clear"
          context:
            correlation_id: "notification_config_lifecycle--clear"

        validations:
          - check: response_schema
            description: "Response matches sync-accounts-response.json schema"
          - check: field_present
            path: "accounts[0].notification_configs"
            description: "The clear operation echoes the now-empty subscriber array"
          - check: field_absent
            path: "accounts[0].notification_configs[0].subscriber_id"
            description: "No subscribers remain after clear"
          - check: field_value
            path: "context.correlation_id"
            value: "notification_config_lifecycle--clear"
            description: "Context correlation_id returned unchanged"

      - id: list_accounts_echoes_cleared_subscribers
        title: "Read cleared subscriber set"
        task: list_accounts
        schema_ref: "account/list-accounts-request.json"
        response_schema_ref: "account/list-accounts-response.json"
        doc_ref: "/accounts/tasks/list_accounts"
        stateful: true
        expected: |
          Return the target account with no persisted notification_configs[]
          entries after the clear operation.

        sample_request:
          account:
            account_id: "$context.notification_config_account_id"
          status: "active"
          sandbox: true
          pagination:
            max_results: 100
          context:
            correlation_id: "notification_config_lifecycle--list_after_clear"

        validations:
          - check: response_schema
            description: "Response matches list-accounts-response.json schema"
          - check: field_value
            path: "accounts[0].account_id"
            value: "$context.notification_config_account_id"
            description: "The configured account is visible on list_accounts after clear"
          - check: field_present
            path: "accounts[0].notification_configs"
            description: "The account echoes an empty subscriber array after clear"
          - check: field_absent
            path: "accounts[0].notification_configs[0].subscriber_id"
            description: "No persisted subscribers remain after clear"
          - check: field_value
            path: "context.correlation_id"
            value: "notification_config_lifecycle--list_after_clear"
            description: "Context correlation_id returned unchanged"
