---
id: ATS-PFM-NOT-002
scope: CAP-PFM-NOT-002
product_area: platform
module: notifications
revision: 2
status: proposed
approved_by: null
approved_at: null
source_sha:
  services-platform: ad3f6a6582843008ed0f5a17b131878d15829af1
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
scenarios:
  - id: ATS-PFM-NOT-002-01
    title: Tie publication to business commit
    level: api
    proves:
      - BR-CAP-PFM-NOT-002-01
      - INV-DOM-PFM-NOT-003-01
    preconditions: Prepare a producer transaction that publishes.
    steps: Roll the transaction back; next commit it.
    expected: Rollback leaves no claimable work; commit leaves exactly one queue entry.
    negative: false
    data_needs: Use only records created and owned by this suite.
    bound_to:
      - >-
        services-platform:modules/notifications/tests/integration/services/dispatch/dispatch-notifications.integration.spec.js#leaves
        nothing to claim when the producer’s transaction rolled back, and one
        row when it committed
      - >-
        services-platform:modules/notifications/tests/integration/db/notification-submissions.table.integration.spec.js#accepts
        the caller’s columns into its own tenant, the negative control of the
        refusals
      - TST-PFM-NOT-071
      - TST-PFM-NOT-075
    provenance: test
    source: >-
      services-platform:modules/notifications/tests/integration/services/dispatch/dispatch-notifications.integration.spec.js#leaves
      nothing to claim when the producer’s transaction rolled back, and one row
      when it committed
    category: invariant
    binding_scope: >-
      Each binding supplies its own assertions; the aggregate references do not
      imply that one executable file proves every step.
    invariant_source: >-
      services-platform:modules/notifications/tests/integration/services/dispatch/dispatch-notifications.integration.spec.js#leaves
      nothing to claim when the producer’s transaction rolled back, and one row
      when it committed
  - id: ATS-PFM-NOT-002-02
    title: Restrict producer access to insertion
    level: api
    proves:
      - BR-CAP-PFM-NOT-002-02
    preconditions: >-
      Prepare the capability role, unable to log in and bound by row-level
      security.
    steps: >-
      Insert into another organization; with no organization set; store the
      claim and order columns; read, change or delete a row; read another
      notification table.
    expected: >-
      Forbidden role operations all fail. Inserting permitted fields in the
      caller's workspace succeeds.
    negative: true
    data_needs: Use only records created and owned by this suite.
    bound_to:
      - >-
        services-platform:modules/notifications/tests/integration/db/notification-submissions.table.integration.spec.js#is
        refused a row in another tenant by the policy, not by the grant
      - >-
        services-platform:modules/notifications/tests/integration/db/notification-submissions.table.integration.spec.js#is
        refused a row when no tenant is set, by the same policy
      - >-
        services-platform:modules/notifications/tests/integration/db/notification-submissions.table.integration.spec.js#may
        not write Platform’s claim column
      - >-
        services-platform:modules/notifications/tests/integration/db/notification-submissions.table.integration.spec.js#may
        not write the order stamp the database fills
      - >-
        services-platform:modules/notifications/tests/integration/db/notification-submissions.table.integration.spec.js#may
        not read, change or delete the queue it writes
      - >-
        services-platform:modules/notifications/tests/integration/db/notification-submissions.table.integration.spec.js#may
        not read or write any other notification table
      - TST-PFM-NOT-071
    provenance: test
    source: >-
      services-platform:modules/notifications/tests/integration/db/notification-submissions.table.integration.spec.js#is
      refused a row in another tenant by the policy, not by the grant
    category: permission
    binding_scope: >-
      Each binding supplies its own assertions; the aggregate references do not
      imply that one executable file proves every step.
  - id: ATS-PFM-NOT-002-03
    title: Deduplicate publication receipts
    level: api
    proves:
      - BR-CAP-PFM-NOT-002-03
      - INV-DOM-PFM-NOT-003-02
    preconditions: Prepare a publish already submitted.
    steps: Submit it a second time with the identical idempotency key.
    expected: >-
      The repeated key returns the original request identifier and duplicate
      indicator, with one queued entry.
    negative: false
    data_needs: Use only records created and owned by this suite.
    bound_to:
      - >-
        services-platform:modules/notifications/tests/integration/services/requests/submit-notification-request.integration.spec.js#answers
        the same request id and a duplicate for a repeat, writing one row
      - >-
        services-platform:modules/notifications/tests/integration/db/notification-submissions.table.integration.spec.js#refuses
        a second row for the same submitter, key and kind as a duplicate, which
        is how a retried transaction asks once
      - >-
        services-platform:modules/notifications/tests/integration/services/dispatch/dispatch-notifications.integration.spec.js#answers
        a repeated submission as a duplicate and queues it once
      - >-
        react-console:acceptance/notifications/playwright/journeys/pipeline/NT-ACC-0200.spec.ts#NT-ACC-0200
      - TST-PFM-NOT-011
      - TST-PFM-NOT-071
      - TST-PFM-NOT-075
      - TST-PFM-NOT-102
    provenance: test
    source: >-
      services-platform:modules/notifications/tests/integration/services/requests/submit-notification-request.integration.spec.js#answers
      the same request id and a duplicate for a repeat, writing one row
    category: invariant
    binding_scope: >-
      Each binding supplies its own assertions; the aggregate references do not
      imply that one executable file proves every step.
    invariant_source: >-
      services-platform:modules/notifications/tests/integration/services/requests/submit-notification-request.integration.spec.js#answers
      the same request id and a duplicate for a repeat, writing one row
  - id: ATS-PFM-NOT-002-04
    title: Apply queue constraints to all writers
    level: api
    proves:
      - BR-CAP-PFM-NOT-002-04
    preconditions: Prepare a writable queue table.
    steps: >-
      Insert rows with a non-version-5 id, a bad submitter, an unrecognized
      kind, a two-word key, a non-object payload, a 33 KB payload; next a cancel
      beside its publish.
    expected: >-
      Invalid entries fail at persistence; cancellation is a separate entry
      sharing the publication identifier.
    negative: true
    data_needs: Use only records created and owned by this suite.
    bound_to:
      - >-
        services-platform:modules/notifications/tests/integration/db/notification-submissions.table.integration.spec.js#refuses
        an id that is not a UUID version 5
      - >-
        services-platform:modules/notifications/tests/integration/db/notification-submissions.table.integration.spec.js#refuses
        a submitter that is not a service name
      - >-
        services-platform:modules/notifications/tests/integration/db/notification-submissions.table.integration.spec.js#refuses
        a kind that is neither publish nor cancel
      - >-
        services-platform:modules/notifications/tests/integration/db/notification-submissions.table.integration.spec.js#refuses
        a definition key that is not three words
      - >-
        services-platform:modules/notifications/tests/integration/db/notification-submissions.table.integration.spec.js#refuses
        a payload that is not an object
      - >-
        services-platform:modules/notifications/tests/integration/db/notification-submissions.table.integration.spec.js#refuses
        a payload over 32 KB
      - >-
        services-platform:modules/notifications/tests/integration/db/notification-submissions.table.integration.spec.js#writes
        a cancel of the same occurrence as a row of its own, under the same
        derived id
      - TST-PFM-NOT-071
    provenance: test
    source: >-
      services-platform:modules/notifications/tests/integration/db/notification-submissions.table.integration.spec.js#refuses
      an id that is not a UUID version 5
    category: boundary
    binding_scope: >-
      Each binding supplies its own assertions; the aggregate references do not
      imply that one executable file proves every step.
  - id: ATS-PFM-NOT-002-05
    title: Reject invalid envelopes before insertion
    level: api
    proves:
      - BR-CAP-PFM-NOT-002-05
    preconditions: Prepare the system door.
    steps: >-
      Submit a invalidly structured envelope; an unrecognized field; a reason of
      201 characters; a reason with a line break.
    expected: Each invalid field is identified and the queue remains unchanged.
    negative: true
    data_needs: Use only records created and owned by this suite.
    bound_to:
      - >-
        services-platform:modules/notifications/tests/integration/services/requests/submit-notification-request.integration.spec.js#refuses
        a malformed envelope naming each path, with nothing written and nobody
        nudged
      - >-
        services-platform:modules/notifications/tests/integration/services/requests/submit-notification-request.integration.spec.js#refuses
        an audience reason of more than 200 characters or with a line break,
        with nothing written
      - >-
        services-platform:modules/notifications/tests/e2e/notifications.e2e.spec.js#refuses
        a malformed envelope with a 400 and an unknown field with a 400
      - TST-PFM-NOT-069
      - TST-PFM-NOT-102
    provenance: test
    source: >-
      services-platform:modules/notifications/tests/integration/services/requests/submit-notification-request.integration.spec.js#refuses
      a malformed envelope naming each path, with nothing written and nobody
      nudged
    category: validation
    binding_scope: >-
      Each binding supplies its own assertions; the aggregate references do not
      imply that one executable file proves every step.
  - id: ATS-PFM-NOT-002-06
    title: Require system publication access
    level: api
    proves:
      - BR-CAP-PFM-NOT-002-06
    preconditions: >-
      Prepare a console caller with every platform key; a system caller with the
      publish permission.
    steps: Submit publication from each caller type.
    expected: >-
      Console access fails; the authorized system caller receives a receipt
      whose processing creates a notice.
    negative: true
    data_needs: Use only records created and owned by this suite.
    bound_to:
      - >-
        services-platform:modules/notifications/tests/e2e/notifications.e2e.spec.js#refuses
        the system door and Rhythm HQ’s routes to a console caller, even one
        holding every platform key
      - >-
        services-platform:modules/notifications/tests/e2e/notifications.e2e.spec.js#writes
        the publish and answers the receipt, which the pipeline then turns into
        a notification
      - >-
        services-platform:modules/notifications/tests/unit/permissions.spec.js#makes
        publish staff-only, so only a system route may require it, and gives it
        to the Rhythm Administrator role
      - TST-PFM-NOT-069
      - TST-PFM-NOT-109
    provenance: test
    source: >-
      services-platform:modules/notifications/tests/e2e/notifications.e2e.spec.js#refuses
      the system door and Rhythm HQ’s routes to a console caller, even one
      holding every platform key
    category: permission
    binding_scope: >-
      Each binding supplies its own assertions; the aggregate references do not
      imply that one executable file proves every step.
  - id: ATS-PFM-NOT-002-07
    title: Require a known cancellation target and reason
    level: api
    proves:
      - BR-CAP-PFM-NOT-002-08
    preconditions: Prepare the system door.
    steps: Cancel an unrecognized id; cancel without a reason.
    expected: >-
      Unknown targets return not found; missing reasons return validation
      rejection.
    negative: true
    data_needs: Use only records created and owned by this suite.
    bound_to:
      - >-
        services-platform:modules/notifications/tests/e2e/notifications.e2e.spec.js#answers
        a cancel of an unknown request 404 and one without a reason 400
      - >-
        services-platform:modules/notifications/tests/integration/services/requests/submit-notification-request.integration.spec.js#is
        not found for an id Platform knows nothing of
      - TST-PFM-NOT-069
      - TST-PFM-NOT-102
    provenance: test
    source: >-
      services-platform:modules/notifications/tests/e2e/notifications.e2e.spec.js#answers
      a cancel of an unknown request 404 and one without a reason 400
    category: validation
    binding_scope: >-
      Each binding supplies its own assertions; the aggregate references do not
      imply that one executable file proves every step.
  - id: ATS-PFM-NOT-002-08
    title: Deduplicate repeated publication end to end
    level: api
    proves:
      - AS-JNY-PFM-NOT-005-02
    preconditions: Prepare a producer transaction.
    steps: Submit an event and repeat its publication.
    expected: Only a single recipient notice is created.
    negative: false
    data_needs: The journey's own personas and Northeast unit.
    bound_to:
      - >-
        react-console:acceptance/notifications/playwright/journeys/pipeline/NT-ACC-0200.spec.ts#NT-ACC-0200
      - TST-PFM-NOT-011
    provenance: test
    source: >-
      react-console:acceptance/notifications/playwright/journeys/pipeline/NT-ACC-0200.spec.ts#NT-ACC-0200
    category: invariant
    binding_scope: >-
      Each binding supplies its own assertions; the aggregate references do not
      imply that one executable file proves every step.
  - id: ATS-PFM-NOT-002-09
    title: Committed publication survives dispatch recovery and withdrawal
    category: interaction
    level: api
    proves:
      - BR-CAP-PFM-NOT-002-01
    exercises:
      - CAP-PFM-NOT-002
      - CAP-PFM-NOT-003
      - CAP-PFM-NOT-006
      - CAP-PFM-NOT-008
    preconditions: Use one responsibility audience and suite-owned request keys.
    steps: >-
      Publish in a rolled-back transaction, then in a committed transaction;
      interrupt dispatch after queue acknowledgement, replay the item and
      withdraw the accepted occurrence.
    expected: >-
      Only the committed occurrence has a request. Replay adds no recipient
      notice, and withdrawal removes the notice from applicable unread results.
    negative: false
    data_needs: >-
      A suite-owned workspace, named staff accounts and unique occurrence keys;
      external adapters record attempts without shared writes.
    bound_to: []
    provenance: proposed
    source:
      - >-
        rhythm-v2-docs:specs/platform/notifications/planning/capabilities/CAP-PFM-NOT-002-tell-people-that-something-happened-with-one-call.md#frontmatter
      - >-
        rhythm-v2-docs:specs/platform/notifications/planning/capabilities/CAP-PFM-NOT-003-reach-the-right-people-and-only-people-of-the-organization.md#frontmatter
      - >-
        rhythm-v2-docs:specs/platform/notifications/planning/capabilities/CAP-PFM-NOT-006-withdraw-or-expire-a-notice-that-no-longer-applies.md#frontmatter
      - >-
        rhythm-v2-docs:specs/platform/notifications/planning/capabilities/CAP-PFM-NOT-008-keep-notifications-whole-when-part-of-the-pipeline-fails.md#frontmatter
    interaction_with:
      - CAP-PFM-NOT-003
      - CAP-PFM-NOT-006
      - CAP-PFM-NOT-008
evidence:
  - >-
    Pinned evidence inventory 1:
    services-platform:modules/notifications/tests/integration/services/dispatch/dispatch-notifications.integration.spec.js;
    services-platform:modules/notifications/tests/integration/services/requests/submit-notification-request.integration.spec.js;
    services-platform:modules/notifications/tests/e2e/notifications.e2e.spec.js
  - >-
    Pinned evidence inventory 2:
    services-platform:modules/notifications/tests/integration/db/notification-submissions.table.integration.spec.js;
    react-console:acceptance/notifications/playwright/journeys/pipeline/NT-ACC-0200.spec.ts;
    services-platform:modules/notifications/tests/unit/permissions.spec.js
coverage:
  rule: 'gap: FND-PFM-NOT-063'
  transition: not-applicable
  validation: covered
  permission: covered
  edge: 'gap: FND-PFM-NOT-063'
  failure: 'gap: FND-PFM-NOT-063'
  boundary: covered
  concurrency: 'gap: FND-PFM-NOT-063'
  invariant: covered
  interaction: covered
coverage_notes:
  transition: >-
    This capability computes or reads an outcome and owns no lifecycle
    transition; state changes are tested in the owning capability.
  rule: >-
    The specification does not isolate a general positive outcome outside the
    narrower category cases.
  edge: Exceptional but valid input needs a dedicated expected-outcome case.
  failure: The suite lacks an isolated dependency failure and recovery case.
  concurrency: >-
    The suite lacks a complete race case with controlled interleaving and
    post-race assertions.
findings:
  - FND-PFM-NOT-063
transition_coverage: []
workflow_coverage:
  - workflow: WF-PFM-NOT-002
    capabilities:
      - CAP-PFM-NOT-002
      - CAP-PFM-NOT-008
      - CAP-PFM-NOT-009
    scenario_candidate: ATS-PFM-NOT-002-09
    coverage: 'gap: FND-PFM-NOT-084'
    reason: >-
      The proposed related-capability sequence does not independently isolate
      every workflow hop, failure seam and interleaving.
  - workflow: WF-PFM-NOT-009
    capabilities:
      - CAP-PFM-NOT-014
      - CAP-PFM-NOT-002
      - CAP-PFM-NOT-006
    scenario_candidate: ATS-PFM-NOT-002-09
    coverage: 'gap: FND-PFM-NOT-084'
    reason: >-
      The proposed related-capability sequence does not independently isolate
      every workflow hop, failure seam and interleaving.
domain_invariant_coverage:
  - invariant: INV-DOM-PFM-NOT-003-01
    scenarios:
      - ATS-PFM-NOT-002-01
    coverage: specified
    source: >-
      rhythm-v2-docs:specs/platform/notifications/design/objects/DOM-PFM-NOT-003-staff-notification.md
    verification_note: >-
      The candidate supplies the fixture outcome; individual binding limits
      remain applicable.
  - invariant: INV-DOM-PFM-NOT-003-02
    scenarios:
      - ATS-PFM-NOT-002-03
    coverage: specified
    source: >-
      rhythm-v2-docs:specs/platform/notifications/design/objects/DOM-PFM-NOT-003-staff-notification.md
    verification_note: >-
      The candidate supplies the fixture outcome; individual binding limits
      remain applicable.
---

# Publish a notification with its business change: Capability Test Specification

## Coverage

Each scenario names the guarantee it proves. Category coverage describes specified cases; executable bindings retain their narrower assertion boundaries. Lifecycle coverage remains a gap wherever guards and effects lack independent verification.

| Rule | Proof |
|---|---|
| `BR-CAP-PFM-NOT-002-01` | `ATS-PFM-NOT-002-01`, `ATS-PFM-NOT-002-09` |
| `BR-CAP-PFM-NOT-002-02` | `ATS-PFM-NOT-002-02` |
| `BR-CAP-PFM-NOT-002-03` | `ATS-PFM-NOT-002-03` |
| `BR-CAP-PFM-NOT-002-04` | `ATS-PFM-NOT-002-04` |
| `BR-CAP-PFM-NOT-002-05` | `ATS-PFM-NOT-002-05` |
| `BR-CAP-PFM-NOT-002-06` | `ATS-PFM-NOT-002-06` |
| `BR-CAP-PFM-NOT-002-08` | `ATS-PFM-NOT-002-07` |

## Interactions With Related Capabilities

Use one responsibility audience and suite-owned request keys. Publish in a rolled-back transaction, then in a committed transaction; interrupt dispatch after queue acknowledgement, replay the item and withdraw the accepted occurrence.

The interaction case combines the producing, processing and reading outcomes named in its frontmatter. It remains proposed until an executable suite performs that complete sequence.
