---
id: ATS-PFM-MTD-019
scope: CAP-PFM-MTD-019
product_area: platform
module: metadata
revision: 2
status: proposed
source_sha:
  services-platform: ad3f6a6582843008ed0f5a17b131878d15829af1
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
scenarios:
  - id: ATS-PFM-MTD-019-01
    title: Deletion policies agree on both sides
    level: api
    proves:
      - BR-CAP-PFM-MTD-019-01
      - INV-DOM-PFM-MTD-006-08
    preconditions: Each rule has current and ended link fixtures.
    steps:
      - do: Delete each endpoint.
        provenance: test
        source: >-
          services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#refuses
          with SQLSTATE AE409 and the message the TypeScript refusal has, removing nothing
    expected: >-
      Detach preserves the other record, cascade removes dependent custom records, restrict protects current
      links and ended links never block deletion.
    negative: true
    data_needs: Each rule has current and ended link fixtures.
    bound_to:
      - >-
        services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#removes
        the association and keeps the record on the other side, answering what it removed
      - >-
        services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#deletes
        the custom record on the other side with its values and its association, and answers it
      - >-
        services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#refuses
        with SQLSTATE AE409 and the message the TypeScript refusal has, removing nothing
      - >-
        services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#removes
        an ended association whatever its rule, and the deletion goes ahead
      - >-
        services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#answers
        what endpointDeletionOutcome says for an open association on the to side under restrict
      - TST-PFM-MTD-037
    provenance: test
    source: >-
      services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#refuses
      with SQLSTATE AE409 and the message the TypeScript refusal has, removing nothing
    category: rule
    assertions:
      - expect: >-
          Detach preserves the other record, cascade removes dependent custom records, restrict protects
          current links and ended links never block deletion.
        provenance: test
        source: >-
          services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#refuses
          with SQLSTATE AE409 and the message the TypeScript refusal has, removing nothing
  - id: ATS-PFM-MTD-019-02
    title: Depth and locks stop unsafe cascades
    level: api
    proves:
      - BR-CAP-PFM-MTD-019-02
    preconditions: 'Chains span the supported depth and exceed it, with another chain containing a lock.'
    steps:
      - do: Delete each head.
        provenance: test
        source: >-
          services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#refuses
          a chain of eleven with SQLSTATE AE409 and removes nothing of it
    expected: Supported chains are removed while excessive or locked chains fail without partial deletion.
    negative: true
    data_needs: 'Chains span the supported depth and exceed it, with another chain containing a lock.'
    bound_to:
      - >-
        services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#deletes
        a chain of ten records, the deepest a cascade may go
      - >-
        services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#refuses
        a chain of eleven with SQLSTATE AE409 and removes nothing of it
      - >-
        services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#refuses
        to delete a locked record in the cascade, naming it, and removes nothing
      - TST-PFM-MTD-037
    provenance: test
    source: >-
      services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#refuses
      a chain of eleven with SQLSTATE AE409 and removes nothing of it
    category: boundary
    assertions:
      - expect: Supported chains are removed while excessive or locked chains fail without partial deletion.
        provenance: test
        source: >-
          services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#refuses
          a chain of eleven with SQLSTATE AE409 and removes nothing of it
  - id: ATS-PFM-MTD-019-03
    title: CRM deletion invokes Platform relationships
    level: api
    proves:
      - BR-CAP-PFM-MTD-019-03
    preconditions: A deployed CRM contact owns a custom boat under a cascading owner type.
    steps:
      - do: Delete the owner through CRM and request the boat and its associations through Platform.
        provenance: test
        source: >-
          services-platform:tests/acceptance/custom-objects/deployed/associations/CO-ACC-0256.spec.js#step 3:
          Fulmar answers 404 and its associations are gone
    expected: 'The owner deletion succeeds, the boat is not found and its associations are gone.'
    negative: false
    data_needs: Deployed CRM endpoints have cascading and restricting links.
    bound_to:
      - >-
        services-platform:tests/acceptance/custom-objects/deployed/associations/CO-ACC-0256.spec.js#step 2:
        Ola is deleted through CRM’s contact delete route: success, and the contact is gone
      - >-
        services-platform:tests/acceptance/custom-objects/deployed/associations/CO-ACC-0256.spec.js#step 3:
        Fulmar answers 404 and its associations are gone
      - >-
        services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#names
        every restricting type, counted and sorted, when several hold the record
      - TST-PFM-MTD-133
      - TST-PFM-MTD-037
    provenance: test
    source: >-
      services-platform:tests/acceptance/custom-objects/deployed/associations/CO-ACC-0256.spec.js#step 3:
      Fulmar answers 404 and its associations are gone
    category: interaction
    proof_limitations:
      - FND-PFM-MTD-082
    assertions:
      - expect: 'The owner deletion succeeds, the boat is not found and its associations are gone.'
        provenance: test
        source: >-
          services-platform:tests/acceptance/custom-objects/deployed/associations/CO-ACC-0256.spec.js#step 3:
          Fulmar answers 404 and its associations are gone
  - id: ATS-PFM-MTD-019-04
    title: Cross-service cascades perform full cleanup
    level: api
    proves:
      - BR-CAP-PFM-MTD-019-04
    preconditions: 'A custom record has values, assets and observable history and event storage.'
    steps:
      - do: Delete its owner through endpoint policy.
        provenance: test
        source: >-
          services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#leaves
          the same audit rows, events, revisions and no record, as the record store does
    expected: 'Dependent cleanup, audit, revisions and events match direct record deletion.'
    negative: false
    data_needs: 'A custom record has values, assets and observable history and event storage.'
    bound_to:
      - >-
        services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#leaves
        the same audit rows, events, revisions and no record, as the record store does
      - >-
        services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#detaches
        the files the record held, due for retirement at once
      - TST-PFM-MTD-037
    provenance: test
    source: >-
      services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#leaves
      the same audit rows, events, revisions and no record, as the record store does
    category: invariant
    assertions:
      - expect: 'Dependent cleanup, audit, revisions and events match direct record deletion.'
        provenance: test
        source: >-
          services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#leaves
          the same audit rows, events, revisions and no record, as the record store does
  - id: ATS-PFM-MTD-019-05
    title: Service execution cannot cross workspaces
    level: api
    proves:
      - BR-CAP-PFM-MTD-019-05
      - INV-DOM-PFM-MTD-006-09
    preconditions: Deletion functions have authorized and unauthorized role contexts.
    steps:
      - do: Call under another workspace or an ungranted database role.
        provenance: test
        source: >-
          services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#refuses
          another tenant than the transaction’s with 42501, removing nothing
    expected: Both calls fail without removing relationships.
    negative: true
    data_needs: Deletion functions have authorized and unauthorized role contexts.
    bound_to:
      - >-
        services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#refuses
        another tenant than the transaction’s with 42501, removing nothing
      - >-
        services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#touches
        no row of another tenant that names the same record id
      - >-
        services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#refuses
        a role that was never granted it (42501)
      - >-
        services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#lets
        the sibling service’s application role call it
      - TST-PFM-MTD-037
    provenance: test
    source: >-
      services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#refuses
      another tenant than the transaction’s with 42501, removing nothing
    category: permission
    assertions:
      - expect: Both calls fail without removing relationships.
        provenance: test
        source: >-
          services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#refuses
          another tenant than the transaction’s with 42501, removing nothing
  - id: ATS-PFM-MTD-019-06
    title: Owner policy survives object archival
    level: api
    proves:
      - BR-CAP-PFM-MTD-019-06
    preconditions: An owner has records of draft or archived custom objects.
    steps:
      - do: Delete the owner under cascading and restricting types.
        provenance: test
        source: >-
          services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#cascades
          to a record of an archived custom object, deleting it
    expected: Cascade removes the archived-object record and restriction preserves the protected record.
    negative: true
    data_needs: An owner has records of draft or archived custom objects.
    bound_to:
      - >-
        services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#cascades
        to a record of an archived custom object, deleting it
      - >-
        services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#refuses
        the owner’s deletion for an open restricting association to a record of a draft custom object
      - TST-PFM-MTD-037
    provenance: test
    source: >-
      services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#cascades
      to a record of an archived custom object, deleting it
    category: interaction
    assertions:
      - expect: Cascade removes the archived-object record and restriction preserves the protected record.
        provenance: test
        source: >-
          services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#cascades
          to a record of an archived custom object, deleting it
  - id: ATS-PFM-MTD-019-07
    title: Merge redirects links and resolves collisions
    level: browser
    proves:
      - BR-CAP-PFM-MTD-019-07
      - INV-DOM-PFM-MTD-006-10
    preconditions: The survivor and duplicate have overlapping pair and primary links.
    steps:
      - do: Merge duplicate endpoints.
        provenance: test
        source: >-
          services-platform:modules/associations/tests/integration/db/associations-merge.integration.spec.js#ends
          m’s association that repeats s’s, giving it s’s id, and leaves s’s as it was
    expected: 'Links name the survivor, colliding or self-links end and an extra primary loses its designation.'
    negative: false
    data_needs: The survivor and duplicate have overlapping pair and primary links.
    bound_to:
      - >-
        services-platform:modules/associations/tests/integration/db/associations-merge.integration.spec.js#moves
        an association m held that s did not have to s, open
      - >-
        services-platform:modules/associations/tests/integration/db/associations-merge.integration.spec.js#ends
        m’s association that repeats s’s, giving it s’s id, and leaves s’s as it was
      - >-
        services-platform:modules/associations/tests/integration/db/associations-merge.integration.spec.js#ends
        the association of s with m, which would link s with itself
      - >-
        services-platform:modules/associations/tests/integration/db/associations-merge.integration.spec.js#moves
        m’s primary association to s as an ordinary one, s keeping its own primary
      - >-
        react-console:acceptance/custom-objects/playwright/journeys/PLAT-M6-S11.spec.ts#PLAT-M6-S11: after CRM
        merges Jane's duplicate her crew places, her boats and Osprey's Previous Owner name the surviving Jane
      - TST-PFM-MTD-039
      - TST-PFM-MTD-025
    provenance: test
    source: >-
      services-platform:modules/associations/tests/integration/db/associations-merge.integration.spec.js#ends
      m’s association that repeats s’s, giving it s’s id, and leaves s’s as it was
    category: interaction
    assertions:
      - expect: 'Links name the survivor, colliding or self-links end and an extra primary loses its designation.'
        provenance: test
        source: >-
          services-platform:modules/associations/tests/integration/db/associations-merge.integration.spec.js#ends
          m’s association that repeats s’s, giving it s’s id, and leaves s’s as it was
  - id: ATS-PFM-MTD-019-08
    title: Merge redirects references with history
    level: api
    proves:
      - BR-CAP-PFM-MTD-019-08
    preconditions: Custom records reference duplicate contacts or organizations.
    steps:
      - do: Merge them and inspect values and history.
        provenance: test
        source: >-
          services-platform:modules/custom-objects/tests/integration/db/custom-object-records-merge.integration.spec.js#names
          the surviving contact in every property that named a merged one
    expected: References name survivors and revisions retain the earlier values.
    negative: false
    data_needs: Custom records reference duplicate contacts or organizations.
    bound_to:
      - >-
        services-platform:modules/custom-objects/tests/integration/db/custom-object-records-merge.integration.spec.js#names
        the surviving contact in every property that named a merged one
      - >-
        services-platform:modules/custom-objects/tests/integration/db/custom-object-records-merge.integration.spec.js#leaves
        a property that already named the survivor, and one naming another contact, as they were
      - >-
        services-platform:modules/custom-objects/tests/integration/db/custom-object-records-merge.integration.spec.js#leaves
        another workspace’s record of the same ids as it was, through both merges
      - TST-PFM-MTD-053
    provenance: test
    source: >-
      services-platform:modules/custom-objects/tests/integration/db/custom-object-records-merge.integration.spec.js#names
      the surviving contact in every property that named a merged one
    category: interaction
    assertions:
      - expect: References name survivors and revisions retain the earlier values.
        provenance: test
        source: >-
          services-platform:modules/custom-objects/tests/integration/db/custom-object-records-merge.integration.spec.js#names
          the surviving contact in every property that named a merged one
  - id: ATS-PFM-MTD-019-09
    title: Merge impact predicts idempotent application
    level: api
    proves:
      - BR-CAP-PFM-MTD-019-09
    preconditions: 'The duplicate has links and references, including collisions and near-limit counts.'
    steps:
      - do: 'Read its plan, apply it and apply it again.'
        provenance: test
        source: >-
          services-platform:modules/associations/tests/integration/db/associations-merge.integration.spec.js#counts
          the associations held by m, and says how many end instead of moving
    expected: >-
      Counts match movements and endings, repetition moves nothing and the per-record cap does not reject the
      merge.
    negative: false
    data_needs: 'The duplicate has links and references, including collisions and near-limit counts.'
    bound_to:
      - >-
        services-platform:modules/associations/tests/integration/db/associations-merge.integration.spec.js#counts
        the associations held by m, and says how many end instead of moving
      - >-
        services-platform:modules/associations/tests/integration/db/associations-merge.integration.spec.js#reports
        the rows the plan promised, column by column
      - >-
        services-platform:modules/custom-objects/tests/integration/db/custom-object-records-merge.integration.spec.js#counts
        the values naming the merged contacts, in the sentence staff read
      - >-
        services-platform:modules/custom-objects/tests/integration/db/custom-object-records-merge.integration.spec.js#moves
        nothing a second time when the same merge is applied again
      - TST-PFM-MTD-039
      - TST-PFM-MTD-053
    provenance: test
    source: >-
      services-platform:modules/associations/tests/integration/db/associations-merge.integration.spec.js#counts
      the associations held by m, and says how many end instead of moving
    category: edge
    assertions:
      - expect: >-
          Counts match movements and endings, repetition moves nothing and the per-record cap does not reject
          the merge.
        provenance: test
        source: >-
          services-platform:modules/associations/tests/integration/db/associations-merge.integration.spec.js#counts
          the associations held by m, and says how many end instead of moving
  - id: ATS-PFM-MTD-019-10
    title: Owner deletion preserves the direct-delete evidence contract
    category: interaction
    level: api
    exercises:
      - CAP-PFM-MTD-019
      - CAP-PFM-MTD-013
      - CAP-PFM-MTD-014
      - CAP-PFM-MTD-015
    proves:
      - BR-CAP-PFM-MTD-019-04
      - BR-CAP-PFM-MTD-013-06
      - BR-CAP-PFM-MTD-014-01
      - BR-CAP-PFM-MTD-015-07
    preconditions: A native owner has a cascading custom record with attachments and values.
    steps:
      - do: >-
          Delete the owner within its service transaction and inspect the dependent record, assets, revisions
          and event order.
        provenance: proposed
        source: &ref_0
          - >-
            services-platform:modules/associations/tests/integration/db/apply-endpoint-deletion.integration.spec.js#leaves
            the same audit rows, events, revisions and no record, as the record store does
          - >-
            services-platform:modules/custom-objects/tests/integration/services/persistence/custom-object-records.integration.spec.js#writes
            every dependant’s event before the record it depended on: the associations, then the cascade
            deepest first (B1)
          - >-
            services-platform:modules/custom-objects/tests/integration/services/history/custom-object-record-history.integration.spec.js#answers
            the association, the update and the creation, newest first
          - >-
            services-platform:modules/custom-objects/tests/integration/services/persistence/custom-object-records.events.integration.spec.js#publishes
            the deletion of the record a cascade takes, with its own object’s number, and the association’s
    expected: Compare cleanup and attribution with an equivalent direct custom-record deletion.
    negative: false
    data_needs: A native owner has a cascading custom record with attachments and values.
    bound_to: []
    provenance: proposed
    source: *ref_0
    assertions:
      - expect: Compare cleanup and attribution with an equivalent direct custom-record deletion.
        provenance: proposed
        source: *ref_0
evidence:
  - >-
    services-platform at ad3f6a6582843008ed0f5a17b131878d15829af1: the endpoint deletion and merge specs and
    the deployed journey named in each scenario.
  - 'react-console at baf6dae382d6f41dce976c7f848667ccc37e076f: Playwright PLAT-M6-S11.'
  - >-
    Codex testing back-fill: pinned capability, domain object, state model and workflow obligations reviewed;
    existing bound test files and exact case titles checked.
coverage:
  rule: covered
  transition: 'gap: FND-PFM-MTD-081'
  validation: 'gap: FND-PFM-MTD-081'
  permission: covered
  edge: covered
  failure: 'gap: FND-PFM-MTD-081'
  boundary: covered
  concurrency: 'gap: FND-PFM-MTD-081'
  invariant: covered
  interaction: covered
coverage_obligations:
  - rule: BR-CAP-PFM-MTD-019-01
    scenarios:
      - ATS-PFM-MTD-019-01
    finding: null
    provenance: documented
    source: >-
      specs/platform/metadata/planning/capabilities/CAP-PFM-MTD-019-apply-each-relationship-s-rule-when-a-linked-record-is-deleted.md
  - rule: BR-CAP-PFM-MTD-019-02
    scenarios:
      - ATS-PFM-MTD-019-02
    finding: null
    provenance: documented
    source: >-
      specs/platform/metadata/planning/capabilities/CAP-PFM-MTD-019-apply-each-relationship-s-rule-when-a-linked-record-is-deleted.md
  - rule: BR-CAP-PFM-MTD-019-03
    scenarios:
      - ATS-PFM-MTD-019-03
    finding: null
    provenance: documented
    source: >-
      specs/platform/metadata/planning/capabilities/CAP-PFM-MTD-019-apply-each-relationship-s-rule-when-a-linked-record-is-deleted.md
  - rule: BR-CAP-PFM-MTD-019-04
    scenarios:
      - ATS-PFM-MTD-019-04
      - ATS-PFM-MTD-019-10
    finding: null
    provenance: documented
    source: >-
      specs/platform/metadata/planning/capabilities/CAP-PFM-MTD-019-apply-each-relationship-s-rule-when-a-linked-record-is-deleted.md
  - rule: BR-CAP-PFM-MTD-019-05
    scenarios:
      - ATS-PFM-MTD-019-05
    finding: null
    provenance: documented
    source: >-
      specs/platform/metadata/planning/capabilities/CAP-PFM-MTD-019-apply-each-relationship-s-rule-when-a-linked-record-is-deleted.md
  - rule: BR-CAP-PFM-MTD-019-06
    scenarios:
      - ATS-PFM-MTD-019-06
    finding: null
    provenance: documented
    source: >-
      specs/platform/metadata/planning/capabilities/CAP-PFM-MTD-019-apply-each-relationship-s-rule-when-a-linked-record-is-deleted.md
  - rule: BR-CAP-PFM-MTD-019-07
    scenarios:
      - ATS-PFM-MTD-019-07
    finding: null
    provenance: documented
    source: >-
      specs/platform/metadata/planning/capabilities/CAP-PFM-MTD-019-apply-each-relationship-s-rule-when-a-linked-record-is-deleted.md
  - rule: BR-CAP-PFM-MTD-019-08
    scenarios:
      - ATS-PFM-MTD-019-08
    finding: null
    provenance: documented
    source: >-
      specs/platform/metadata/planning/capabilities/CAP-PFM-MTD-019-apply-each-relationship-s-rule-when-a-linked-record-is-deleted.md
  - rule: BR-CAP-PFM-MTD-019-09
    scenarios:
      - ATS-PFM-MTD-019-09
    finding: null
    provenance: documented
    source: >-
      specs/platform/metadata/planning/capabilities/CAP-PFM-MTD-019-apply-each-relationship-s-rule-when-a-linked-record-is-deleted.md
  - rule: BR-CAP-PFM-MTD-019-10
    scenarios: []
    finding: FND-PFM-MTD-081
    provenance: documented
    source: >-
      specs/platform/metadata/planning/capabilities/CAP-PFM-MTD-019-apply-each-relationship-s-rule-when-a-linked-record-is-deleted.md
interactions:
  - with: CAP-PFM-MTD-013
    scenarios:
      - ATS-PFM-MTD-019-10
    finding: null
    provenance: proposed
    source: >-
      specs/platform/metadata/planning/capabilities/CAP-PFM-MTD-019-apply-each-relationship-s-rule-when-a-linked-record-is-deleted.md
  - with: CAP-PFM-MTD-014
    scenarios:
      - ATS-PFM-MTD-019-10
    finding: null
    provenance: proposed
    source: >-
      specs/platform/metadata/planning/capabilities/CAP-PFM-MTD-019-apply-each-relationship-s-rule-when-a-linked-record-is-deleted.md
---

# Preserve Relationships Through Deletion and Merge: Capability Test Specification

## Coverage

- Obligation 01: 1 scenario cite this obligation.
- Obligation 02: 1 scenario cite this obligation.
- Obligation 03: 1 scenario cite this obligation.
- Obligation 04: 2 scenarios cite this obligation.
- Obligation 05: 1 scenario cite this obligation.
- Obligation 06: 1 scenario cite this obligation.
- Obligation 07: 1 scenario cite this obligation.
- Obligation 08: 1 scenario cite this obligation.
- Obligation 09: 1 scenario cite this obligation.
- Obligation 10: The coverage finding records the missing scenario.

Category coverage identifies the kind of proof present. Detailed transition, permission and invariant obligations and unbound proposals remain in the coverage finding until their individual cases are accounted for.

## Interactions With Related Capabilities

The service boundary preserves dependent deletion evidence and cleanup. The structured interaction rows identify the related capabilities exercised and any remaining gap. Cross-capability workflow seams remain subject to the detailed coverage audit.
