---
id: ATS-CRM-ENT-015
scope: CAP-CRM-ENT-015
product_area: crm
module: entities
revision: 1
status: proposed
source_sha:
  services-crm: a8d1364f1dd44b723bb5f9acba12f983e9ec8d69
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
scenarios:
  - id: ATS-CRM-ENT-015-01
    title: Lock and unlock need their own administrator keys
    level: browser
    proves:
      - BR-CAP-CRM-ENT-015-01
    preconditions: No additional setup is required.
    steps: Read the route declarations; call the routes as a standard user.
    expected: Lock and unlock keys per concept at tier admin; 403.
    negative: true
    data_needs: No additional fixtures.
    bound_to:
      - TST-CRM-ENT-774
    provenance: test
    source: >-
      services-crm:modules/entities/tests/unit/api/record-lock-api-mapper.spec.js#publishes
      all six at tier admin, so administrators hold them and ordinary users do
      not
    category: permission
    action_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/unit/api/record-lock-api-mapper.spec.js#publishes
        all six at tier admin, so administrators hold them and ordinary users do
        not
    assertion_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/unit/api/record-lock-api-mapper.spec.js#publishes
        all six at tier admin, so administrators hold them and ordinary users do
        not
    test_anchors:
      - >-
        services-crm:modules/entities/tests/unit/api/record-lock-api-mapper.spec.js#requires
        crm.contacts.lock and crm.contacts.unlock, and the read key for the
        audit
      - >-
        services-crm:modules/entities/tests/unit/api/record-lock-api-mapper.spec.js#requires
        crm.organizations.lock and crm.organizations.unlock
      - >-
        services-crm:modules/entities/tests/unit/api/record-lock-api-mapper.spec.js#requires
        crm.contact-roles.lock and crm.contact-roles.unlock
      - >-
        services-crm:modules/entities/tests/unit/api/record-lock-api-mapper.spec.js#publishes
        all six at tier admin, so administrators hold them and ordinary users do
        not
      - >-
        react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0035.spec.ts#CRM-ACC-0035
    binding_scope: >-
      contributions: the cited executable assertions collectively support only
      the observations they contain; additional matrix and journey checks remain
      proposed.
  - id: ATS-CRM-ENT-015-02
    title: 'Version, reason and no-op'
    level: api
    proves:
      - BR-CAP-CRM-ENT-015-02
      - INV-DOM-CRM-ENT-033-05
    preconditions: Prepare a locked role.
    steps: >-
      Unlock at a stale version; with a blank reason; an unknown record; lock
      with the lock it has.
    expected: 409; 400 naming reason; 404; nothing changed or recorded.
    negative: true
    data_needs: No additional fixtures.
    bound_to:
      - TST-CRM-ENT-794
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#leaves
      the role at its version and lock, and records nothing
    category: edge
    action_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#leaves
        the role at its version and lock, and records nothing
    assertion_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#leaves
        the role at its version and lock, and records nothing
    test_anchors:
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
        a stale version as stale
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
        a blank reason, naming the reason
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
        an unknown record as not found
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#answers
        that a lock the record already has changed nothing
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#leaves
        the role at its version and lock, and records nothing
      - >-
        services-crm:modules/entities/tests/unit/api/record-lock-api-mapper.spec.js#refuses
        a lock without the version read
    binding_scope: >-
      contributions: the cited executable assertions collectively support only
      the observations they contain; additional matrix and journey checks remain
      proposed.
  - id: ATS-CRM-ENT-015-03
    title: Lock changes are audited and listed
    level: api
    proves:
      - BR-CAP-CRM-ENT-015-03
    preconditions: Prepare a role locked and unlocked.
    steps: Read its lock changes in pages.
    expected: >-
      The newest first with every property, then the older one, with the version
      advanced on each change.
    negative: false
    data_needs: No additional fixtures.
    bound_to:
      - TST-CRM-ENT-794
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.queries.integration.spec.js#answers
      the newest two changes of the record, every property mapped, and where the
      page ended
    category: rule
    action_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.queries.integration.spec.js#answers
        the newest two changes of the record, every property mapped, and where
        the page ended
    assertion_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.queries.integration.spec.js#answers
        the newest two changes of the record, every property mapped, and where
        the page ended
    test_anchors:
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#records
        who unlocked it, the lock it had and why, the reason trimmed
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#records
        both changes, in order
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.queries.integration.spec.js#answers
        the newest two changes of the record, every property mapped, and where
        the page ended
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.queries.integration.spec.js#answers
        the oldest change on the page after, with no further page
    binding_scope: >-
      contributions: the cited executable assertions collectively support only
      the observations they contain; additional matrix and journey checks remain
      proposed.
  - id: ATS-CRM-ENT-015-04
    title: Ordinary writes cannot set a lock
    level: api
    proves:
      - BR-CAP-CRM-ENT-015-04
      - INV-DOM-CRM-ENT-033-06
    preconditions: 'Prepare a contact, an organization and a contact role.'
    steps: >-
      Create, replace and patch with sys_locked set; a role write with a delete
      lock and with a full lock.
    expected: 403 naming sys_locked; delete lock accepted; full lock rejected.
    negative: true
    data_needs: No additional fixtures.
    bound_to:
      - TST-CRM-ENT-786
    provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 403
      naming sys_locked for a replacement that sets a lock (E-14)
    category: permission
    action_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 403
        naming sys_locked for a replacement that sets a lock (E-14)
    assertion_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 403
        naming sys_locked for a replacement that sets a lock (E-14)
    test_anchors:
      - >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 403
        naming sys_locked for a create that sets a lock (E-14)
      - >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 403
        naming sys_locked for a replacement that sets a lock (E-14)
      - >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 403
        naming sys_locked for a patch that sets a lock (E-14)
      - >-
        services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js#answers
        403 naming sys_locked for a value that is no lock
      - >-
        services-crm:modules/entities/tests/unit/api/record-lock-api-mapper.spec.js#refuses
        a contact role create that sets a full lock, as before
      - >-
        services-crm:modules/entities/tests/unit/api/record-lock-api-mapper.spec.js#still
        takes a lock against deletes on a contact role write
    binding_scope: >-
      contributions: the cited executable assertions collectively support only
      the observations they contain; additional matrix and journey checks remain
      proposed.
  - id: ATS-CRM-ENT-015-05
    title: Full and delete locks refuse what they name
    level: browser
    proves:
      - BR-CAP-CRM-ENT-015-05
      - INV-DOM-CRM-ENT-033-04
    preconditions: Prepare a contact locked against updates and deletes; one against deletes.
    steps: Replace each; delete each.
    expected: >-
      409 for the first replacement and both deletes; the second replacement
      saved.
    negative: true
    data_needs: No additional fixtures.
    bound_to:
      - TST-CRM-ENT-786
      - TST-CRM-ENT-794
      - TST-CRM-ENT-775
    provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409
      IllegalOperation, not 403, for a replacement of a contact locked against
      updates, the lock sent back unchanged
    category: invariant
    action_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409
        IllegalOperation, not 403, for a replacement of a contact locked against
        updates, the lock sent back unchanged
    assertion_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409
        IllegalOperation, not 403, for a replacement of a contact locked against
        updates, the lock sent back unchanged
    test_anchors:
      - >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409
        IllegalOperation, not 403, for a replacement of a contact locked against
        updates, the lock sent back unchanged
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
        an ordinary replacement while it is locked
      - >-
        react-console:acceptance/crm/playwright/journeys/organizations/CRM-ACC-0129.spec.ts#CRM-ACC-0129
    binding_scope: >-
      contributions: the cited executable assertions collectively support only
      the observations they contain; additional matrix and journey checks remain
      proposed.
  - id: ATS-CRM-ENT-015-06
    title: Element locks and their release
    level: api
    proves:
      - BR-CAP-CRM-ENT-015-06
    preconditions: Prepare a contact and an organization with locked elements.
    steps: Unlock with no element ids; with one id; with an id the record lacks.
    expected: All released and audited; only that one; rejected releasing nothing.
    negative: true
    data_needs: No additional fixtures.
    bound_to:
      - TST-CRM-ENT-786
      - TST-CRM-ENT-794
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      an element id the organization does not have, releasing nothing
    category: transition
    action_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
        an element id the organization does not have, releasing nothing
    assertion_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
        an element id the organization does not have, releasing nothing
    test_anchors:
      - >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#releases
        the locks of the contact elements with the unlock action, all of them
        when none is named (CRM-ACC-0035)
      - >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#releases
        only the element ids the unlock names, and refuses an id the contact has
        no element for (CRM-ACC-0035)
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
        an element id the organization does not have, releasing nothing
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#audits
        each released element with the reason, though the contact itself was not
        locked
    binding_scope: >-
      contributions: the cited executable assertions collectively support only
      the observations they contain; additional matrix and journey checks remain
      proposed.
  - id: ATS-CRM-ENT-015-07
    title: Clearing an element lock by write needs the unlock key
    level: api
    proves:
      - BR-CAP-CRM-ENT-015-07
    preconditions: Prepare a contact with a locked element.
    steps: >-
      Replace clearing only that lock without and with the unlock key; patch it
      to null.
    expected: 409 then 200; 200.
    negative: true
    data_needs: No additional fixtures.
    bound_to:
      - TST-CRM-ENT-786
    provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409
      to a replacement that only clears an element lock without the unlock key,
      and 200 with it (CRM-ACC-0035)
    category: permission
    action_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409
        to a replacement that only clears an element lock without the unlock
        key, and 200 with it (CRM-ACC-0035)
    assertion_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409
        to a replacement that only clears an element lock without the unlock
        key, and 200 with it (CRM-ACC-0035)
    test_anchors:
      - >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409
        to a replacement that only clears an element lock without the unlock
        key, and 200 with it (CRM-ACC-0035)
      - >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#lets a
        patch clear one element lock by setting it to null (CRM-ACC-0035)
      - >-
        services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js#answers
        409 to a replacement that only clears an element lock without the unlock
        key, and 200 with it
    binding_scope: >-
      contributions: the cited executable assertions collectively support only
      the observations they contain; additional matrix and journey checks remain
      proposed.
  - id: ATS-CRM-ENT-015-08
    title: Protected system records
    level: api
    proves:
      - BR-CAP-CRM-ENT-015-08
      - INV-DOM-CRM-ENT-033-07
    preconditions: Prepare a protected role.
    steps: >-
      Unlock it; change its lock; replace it dropping its lock; rename it;
      delete it.
    expected: >-
      Rejected naming the protection; rejected; rejected; renamed with the lock
      preserved; rejected.
    negative: true
    data_needs: Use a protected_records row.
    bound_to:
      - TST-CRM-ENT-794
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      the unlock, naming the protection
    category: invariant
    action_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
        the unlock, naming the protection
    assertion_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
        the unlock, naming the protection
    test_anchors:
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
        the unlock, naming the protection
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
        a lock change too
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
        an ordinary replacement that would drop its lock
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#still
        lets the customer rename it, keeping its lock
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
        the delete, naming the protection
    binding_scope: >-
      contributions: the cited executable assertions collectively support only
      the observations they contain; additional matrix and journey checks remain
      proposed.
  - id: ATS-CRM-ENT-015-09
    title: Check the lock reason at its exact length boundary
    category: boundary
    level: api
    proves:
      - BR-CAP-CRM-ENT-015-02
    preconditions: >-
      Two unlocked records have current versions and an authorized lock
      operator.
    steps: >-
      Lock one with a 1,000-character reason and the other with 1,001
      characters.
    expected: >-
      The first lock records its reason; the second is refused without a record
      or audit change.
    negative: true
    data_needs: Run-owned records and configuration in an isolated workspace.
    bound_to: []
    provenance: proposed
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#leaves
      the role at its version and lock, and records nothing
    test_anchors: []
    action_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#leaves
        the role at its version and lock, and records nothing
    assertion_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#leaves
        the role at its version and lock, and records nothing
    binding_scope: 'unbound: the detailed scenario still needs executable proof.'
  - id: ATS-CRM-ENT-015-10
    title: A full lock blocks an ordinary correction
    category: interaction
    level: api
    proves:
      - BR-CAP-CRM-ENT-004-01
      - BR-CAP-CRM-ENT-015-05
    preconditions: An editor has read a record and an administrator can change its lock.
    steps: >-
      Apply a full lock; attempt the pending ordinary correction; unlock and
      reread before saving again.
    expected: >-
      The locked attempt preserves the record; a newly based correction succeeds
      after release.
    negative: false
    data_needs: Run-owned records and configuration in an isolated workspace.
    bound_to: []
    provenance: proposed
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400
      naming sys_version to a patch that tests no version;
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409
      IllegalOperation, not 403, for a replacement of a contact locked against
      updates, the lock sent back unchanged
    exercises:
      - CAP-CRM-ENT-004
      - CAP-CRM-ENT-015
    workflows: []
    test_anchors: []
    action_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400
        naming sys_version to a patch that tests no version;
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409
        IllegalOperation, not 403, for a replacement of a contact locked against
        updates, the lock sent back unchanged
    assertion_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400
        naming sys_version to a patch that tests no version;
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409
        IllegalOperation, not 403, for a replacement of a contact locked against
        updates, the lock sent back unchanged
    binding_scope: 'unbound: the detailed scenario still needs executable proof.'
  - id: ATS-CRM-ENT-015-11
    title: Unlock the blocking element before deletion
    category: interaction
    level: api
    proves:
      - BR-CAP-CRM-ENT-014-04
      - BR-CAP-CRM-ENT-015-06
    preconditions: >-
      A person has an element carrying a deletion lock and an administrator has
      delete and unlock authority.
    steps: >-
      Try deleting; release the specific element lock at the current version;
      retry deletion.
    expected: >-
      The first attempt retains the person; the audited release allows deletion
      if no other dependency blocks it.
    negative: false
    data_needs: Run-owned records and configuration in an isolated workspace.
    bound_to: []
    provenance: proposed
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      the delete as IllegalOperation, naming the element that is locked;
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      an element id the organization does not have, releasing nothing
    exercises:
      - CAP-CRM-ENT-014
      - CAP-CRM-ENT-015
    workflows:
      - WF-CRM-ENT-009
    test_anchors: []
    action_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
        the delete as IllegalOperation, naming the element that is locked;
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
        an element id the organization does not have, releasing nothing
    assertion_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
        the delete as IllegalOperation, naming the element that is locked;
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
        an element id the organization does not have, releasing nothing
    binding_scope: 'unbound: the detailed scenario still needs executable proof.'
  - id: ATS-CRM-ENT-015-12
    title: Lock changes appear in audit and aggregate history
    category: interaction
    level: api
    proves:
      - BR-CAP-CRM-ENT-015-03
      - BR-CAP-CRM-ENT-016-01
    preconditions: A live person has no lock and both histories are readable.
    steps: Apply a delete lock with a reason and read lock audit and record history.
    expected: >-
      Both reads identify the lock change without creating multiple aggregate
      revisions.
    negative: false
    data_needs: Run-owned records and configuration in an isolated workspace.
    bound_to: []
    provenance: proposed
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.queries.integration.spec.js#answers
      the newest two changes of the record, every property mapped, and where the
      page ended;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.history.integration.spec.js#writes
      exactly one revision, whatever the save touched
    exercises:
      - CAP-CRM-ENT-015
      - CAP-CRM-ENT-016
    workflows:
      - WF-CRM-ENT-009
    test_anchors: []
    action_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.queries.integration.spec.js#answers
        the newest two changes of the record, every property mapped, and where
        the page ended;
        services-crm:modules/entities/tests/integration/services/persistence/contacts.history.integration.spec.js#writes
        exactly one revision, whatever the save touched
    assertion_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.queries.integration.spec.js#answers
        the newest two changes of the record, every property mapped, and where
        the page ended;
        services-crm:modules/entities/tests/integration/services/persistence/contacts.history.integration.spec.js#writes
        exactly one revision, whatever the save touched
    binding_scope: 'unbound: the detailed scenario still needs executable proof.'
  - id: ATS-CRM-ENT-015-13
    title: Legacy role writes differ from dedicated lock actions
    category: interaction
    level: api
    proves:
      - BR-CAP-CRM-ENT-015-04
      - BR-CAP-CRM-ENT-017-04
    preconditions: >-
      A role is available for ordinary editing and dedicated lock
      administration.
    steps: >-
      Set a delete lock through the ordinary role write; compare its audit with
      a later dedicated lock action.
    expected: >-
      The ordinary role write retains the documented delete-lock exception; the
      dedicated action records its own reason and audit.
    negative: false
    data_needs: Run-owned records and configuration in an isolated workspace.
    bound_to: []
    provenance: proposed
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 403
      naming sys_locked for a replacement that sets a lock (E-14);
      react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0196.spec.ts#CRM-ACC-0196
    exercises:
      - CAP-CRM-ENT-015
      - CAP-CRM-ENT-017
    workflows: []
    test_anchors: []
    action_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 403
        naming sys_locked for a replacement that sets a lock (E-14);
        react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0196.spec.ts#CRM-ACC-0196
    assertion_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 403
        naming sys_locked for a replacement that sets a lock (E-14);
        react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0196.spec.ts#CRM-ACC-0196
    binding_scope: 'unbound: the detailed scenario still needs executable proof.'
  - id: ATS-CRM-ENT-015-14
    title: A locked classification blocks its ordinary mutation
    category: interaction
    level: api
    proves:
      - BR-CAP-CRM-ENT-015-09
      - BR-CAP-CRM-ENT-018-07
    preconditions: A classification is already locked against updates.
    steps: >-
      Attempt a classification correction and deletion through its ordinary
      routes.
    expected: >-
      Both operations are refused and the classification remains; this catalogue
      has no dedicated release action.
    negative: false
    data_needs: Run-owned records and configuration in an isolated workspace.
    bound_to: []
    provenance: proposed
    source: >-
      services-crm:docs/record-locks.md#Not covered yet;
      services-crm:modules/entities/tests/e2e/contact-types.e2e.spec.js#then,
      when the version is off, refuses the change (409)
    exercises:
      - CAP-CRM-ENT-015
      - CAP-CRM-ENT-018
    workflows: []
    test_anchors: []
    action_evidence:
      provenance: proposed
      source: >-
        services-crm:docs/record-locks.md#Not covered yet;
        services-crm:modules/entities/tests/e2e/contact-types.e2e.spec.js#then,
        when the version is off, refuses the change (409)
    assertion_evidence:
      provenance: proposed
      source: >-
        services-crm:docs/record-locks.md#Not covered yet;
        services-crm:modules/entities/tests/e2e/contact-types.e2e.spec.js#then,
        when the version is off, refuses the change (409)
    binding_scope: 'unbound: the detailed scenario still needs executable proof.'
  - id: ATS-CRM-ENT-015-15
    title: Exercise each dedicated customer-lock change
    category: transition
    level: api
    proves:
      - BR-CAP-CRM-ENT-015-02
      - BR-CAP-CRM-ENT-015-03
      - BR-CAP-CRM-ENT-015-05
    preconditions: >-
      Run-owned unlocked contact and organization records are available to a
      lock operator.
    steps: >-
      For each record, apply delete lock, strengthen to full lock, weaken to
      delete lock and release it. On another record, apply full lock directly
      and release it. Read the current version before each action and inspect
      both audits.
    expected: >-
      Every changed lock advances its owner version and contributes a reasoned
      audit; reads reflect each requested lock state.
    negative: false
    data_needs: Run-owned records and configuration in an isolated workspace.
    bound_to: []
    provenance: proposed
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.queries.integration.spec.js#answers
      the newest two changes of the record, every property mapped, and where the
      page ended;
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 403
      naming sys_locked for a replacement that sets a lock (E-14);
      services-crm:modules/entities/services/persistence/record-locks.ts:changeRecordLock;
      services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
      an element id the organization does not have, releasing nothing;
      services-crm:modules/entities/services/persistence/record-locks.ts:changeRecordLock;
      services-crm:modules/entities/services/persistence/record-locks.ts:changeRecordLock
    test_anchors: []
    action_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.queries.integration.spec.js#answers
        the newest two changes of the record, every property mapped, and where
        the page ended;
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 403
        naming sys_locked for a replacement that sets a lock (E-14);
        services-crm:modules/entities/services/persistence/record-locks.ts:changeRecordLock;
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
        an element id the organization does not have, releasing nothing;
        services-crm:modules/entities/services/persistence/record-locks.ts:changeRecordLock;
        services-crm:modules/entities/services/persistence/record-locks.ts:changeRecordLock
    assertion_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.queries.integration.spec.js#answers
        the newest two changes of the record, every property mapped, and where
        the page ended;
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 403
        naming sys_locked for a replacement that sets a lock (E-14);
        services-crm:modules/entities/services/persistence/record-locks.ts:changeRecordLock;
        services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
        an element id the organization does not have, releasing nothing;
        services-crm:modules/entities/services/persistence/record-locks.ts:changeRecordLock;
        services-crm:modules/entities/services/persistence/record-locks.ts:changeRecordLock
    binding_scope: 'unbound: the detailed scenario still needs executable proof.'
  - id: ATS-CRM-ENT-015-16
    title: Exercise every declared authority independently
    category: permission
    level: api
    proves:
      - BR-CAP-CRM-ENT-015-01
      - BR-CAP-CRM-ENT-015-04
      - BR-CAP-CRM-ENT-015-07
    preconditions: >-
      An eligible record and configuration fixture exist for each operation; the
      caller can reach only the run’s workspace.
    steps: >-
      For each declared permission, execute its matching operation with that
      permission, then without it while retaining the other required grants.
      Read affected records after every refused mutation.
    expected: >-
      The authorized case reaches the operation; the missing-grant case is
      refused and preserves saved data. Record access alone does not authorize
      the other declared actions.
    negative: true
    data_needs: Run-owned records and configuration in an isolated workspace.
    bound_to: []
    provenance: proposed
    source: >-
      services-crm:aws/crm.service.ts:ApplicationPermissions;
      services-crm:modules/entities/aws/api/entities.routes.ts
    permission_keys:
      - crm.contacts.lock
      - crm.contacts.unlock
      - crm.organizations.lock
      - crm.organizations.unlock
      - crm.contact-roles.lock
      - crm.contact-roles.unlock
      - crm.contacts.read
      - crm.organizations.read
      - crm.contact-roles.read
    test_anchors: []
    action_evidence:
      provenance: proposed
      source: >-
        services-crm:aws/crm.service.ts:ApplicationPermissions;
        services-crm:modules/entities/aws/api/entities.routes.ts
    assertion_evidence:
      provenance: proposed
      source: >-
        services-crm:aws/crm.service.ts:ApplicationPermissions;
        services-crm:modules/entities/aws/api/entities.routes.ts
    binding_scope: 'unbound: the detailed scenario still needs executable proof.'
evidence:
  - >-
    services-crm:modules/entities/tests/unit/api/record-lock-api-mapper.spec.js#publishes
    all six at tier admin, so administrators hold them and ordinary users do not
  - >-
    services-crm:modules/entities/tests/unit/api/record-lock-api-mapper.spec.js#requires
    crm.contacts.lock and crm.contacts.unlock, and the read key for the audit
  - >-
    services-crm:modules/entities/tests/unit/api/record-lock-api-mapper.spec.js#requires
    crm.organizations.lock and crm.organizations.unlock
  - >-
    services-crm:modules/entities/tests/unit/api/record-lock-api-mapper.spec.js#requires
    crm.contact-roles.lock and crm.contact-roles.unlock
  - >-
    react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0035.spec.ts#CRM-ACC-0035
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#leaves
    the role at its version and lock, and records nothing
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
    a stale version as stale
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
    a blank reason, naming the reason
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
    an unknown record as not found
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#answers
    that a lock the record already has changed nothing
  - >-
    services-crm:modules/entities/tests/unit/api/record-lock-api-mapper.spec.js#refuses
    a lock without the version read
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/record-locks.queries.integration.spec.js#answers
    the newest two changes of the record, every property mapped, and where the
    page ended
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#records
    who unlocked it, the lock it had and why, the reason trimmed
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#records
    both changes, in order
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/record-locks.queries.integration.spec.js#answers
    the oldest change on the page after, with no further page
  - >-
    services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 403
    naming sys_locked for a replacement that sets a lock (E-14)
  - >-
    services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 403
    naming sys_locked for a create that sets a lock (E-14)
  - >-
    services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 403
    naming sys_locked for a patch that sets a lock (E-14)
  - >-
    services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js#answers
    403 naming sys_locked for a value that is no lock
  - >-
    services-crm:modules/entities/tests/unit/api/record-lock-api-mapper.spec.js#refuses
    a contact role create that sets a full lock, as before
  - >-
    services-crm:modules/entities/tests/unit/api/record-lock-api-mapper.spec.js#still
    takes a lock against deletes on a contact role write
  - >-
    services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409
    IllegalOperation, not 403, for a replacement of a contact locked against
    updates, the lock sent back unchanged
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
    an ordinary replacement while it is locked
  - >-
    react-console:acceptance/crm/playwright/journeys/organizations/CRM-ACC-0129.spec.ts#CRM-ACC-0129
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
    an element id the organization does not have, releasing nothing
  - >-
    services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#releases the
    locks of the contact elements with the unlock action, all of them when none
    is named (CRM-ACC-0035)
  - >-
    services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#releases only
    the element ids the unlock names, and refuses an id the contact has no
    element for (CRM-ACC-0035)
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#audits
    each released element with the reason, though the contact itself was not
    locked
  - >-
    services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409 to
    a replacement that only clears an element lock without the unlock key, and
    200 with it (CRM-ACC-0035)
  - >-
    services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#lets a patch
    clear one element lock by setting it to null (CRM-ACC-0035)
  - >-
    services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js#answers
    409 to a replacement that only clears an element lock without the unlock
    key, and 200 with it
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
    the unlock, naming the protection
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
    a lock change too
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
    an ordinary replacement that would drop its lock
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#still
    lets the customer rename it, keeping its lock
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
    the delete, naming the protection
  - >-
    services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400
    naming sys_version to a patch that tests no version;
    services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409
    IllegalOperation, not 403, for a replacement of a contact locked against
    updates, the lock sent back unchanged
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
    the delete as IllegalOperation, naming the element that is locked;
    services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
    an element id the organization does not have, releasing nothing
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/record-locks.queries.integration.spec.js#answers
    the newest two changes of the record, every property mapped, and where the
    page ended;
    services-crm:modules/entities/tests/integration/services/persistence/contacts.history.integration.spec.js#writes
    exactly one revision, whatever the save touched
  - >-
    services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 403
    naming sys_locked for a replacement that sets a lock (E-14);
    react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0196.spec.ts#CRM-ACC-0196
  - >-
    services-crm:docs/record-locks.md#Not covered yet;
    services-crm:modules/entities/tests/e2e/contact-types.e2e.spec.js#then, when
    the version is off, refuses the change (409)
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/record-locks.queries.integration.spec.js#answers
    the newest two changes of the record, every property mapped, and where the
    page ended;
    services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 403
    naming sys_locked for a replacement that sets a lock (E-14);
    services-crm:modules/entities/services/persistence/record-locks.ts:changeRecordLock;
    services-crm:modules/entities/tests/integration/services/persistence/record-locks.integration.spec.js#refuses
    an element id the organization does not have, releasing nothing;
    services-crm:modules/entities/services/persistence/record-locks.ts:changeRecordLock;
    services-crm:modules/entities/services/persistence/record-locks.ts:changeRecordLock
  - >-
    services-crm:aws/crm.service.ts:ApplicationPermissions;
    services-crm:modules/entities/aws/api/entities.routes.ts
coverage:
  rule: covered
  transition: covered
  validation: 'gap: FND-CRM-ENT-063'
  permission: covered
  edge: covered
  failure: 'gap: FND-CRM-ENT-063'
  boundary: covered
  concurrency: 'gap: FND-CRM-ENT-063'
  invariant: covered
  interaction: covered
previous_approval:
  revision: 1
  approved_by: Andrew
  approved_at: '2026-10-22T00:00:00.000Z'
transition_coverage:
  - state_model: STATE-CRM-ENT-007
    from: unlocked
    to: delete-locked
    scenarios:
      - ATS-CRM-ENT-015-15
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/design/state-models/STATE-CRM-ENT-007-record-lock.md:transitions
  - state_model: STATE-CRM-ENT-007
    from: unlocked
    to: fully locked
    scenarios:
      - ATS-CRM-ENT-015-15
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/design/state-models/STATE-CRM-ENT-007-record-lock.md:transitions
  - state_model: STATE-CRM-ENT-007
    from: delete-locked
    to: fully locked
    scenarios:
      - ATS-CRM-ENT-015-15
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/design/state-models/STATE-CRM-ENT-007-record-lock.md:transitions
  - state_model: STATE-CRM-ENT-007
    from: fully locked
    to: unlocked
    scenarios:
      - ATS-CRM-ENT-015-15
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/design/state-models/STATE-CRM-ENT-007-record-lock.md:transitions
  - state_model: STATE-CRM-ENT-007
    from: delete-locked
    to: unlocked
    scenarios:
      - ATS-CRM-ENT-015-15
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/design/state-models/STATE-CRM-ENT-007-record-lock.md:transitions
  - state_model: STATE-CRM-ENT-007
    from: fully locked
    to: delete-locked
    scenarios:
      - ATS-CRM-ENT-015-15
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/design/state-models/STATE-CRM-ENT-007-record-lock.md:transitions
coverage_notes:
  validation:
    reason: >-
      No detailed validation scenario currently specifies this aspect; the
      coverage finding records the required follow-through.
    provenance: unknown
    source: FND-CRM-ENT-063
  failure:
    reason: >-
      No detailed failure scenario currently specifies this aspect; the coverage
      finding records the required follow-through.
    provenance: unknown
    source: FND-CRM-ENT-063
  concurrency:
    reason: >-
      No detailed concurrency scenario currently specifies this aspect; the
      coverage finding records the required follow-through.
    provenance: unknown
    source: FND-CRM-ENT-063
coverage_findings:
  - FND-CRM-ENT-063
  - FND-CRM-ENT-075
permission_coverage:
  - key: crm.contacts.lock
    where_checked: api
    scenario: ATS-CRM-ENT-015-16
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/planning/capabilities/CAP-CRM-ENT-015-lock-records-against-change-and-protect-the-records-rhythm.md
  - key: crm.contacts.unlock
    where_checked: api
    scenario: ATS-CRM-ENT-015-16
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/planning/capabilities/CAP-CRM-ENT-015-lock-records-against-change-and-protect-the-records-rhythm.md
  - key: crm.organizations.lock
    where_checked: api
    scenario: ATS-CRM-ENT-015-16
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/planning/capabilities/CAP-CRM-ENT-015-lock-records-against-change-and-protect-the-records-rhythm.md
  - key: crm.organizations.unlock
    where_checked: api
    scenario: ATS-CRM-ENT-015-16
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/planning/capabilities/CAP-CRM-ENT-015-lock-records-against-change-and-protect-the-records-rhythm.md
  - key: crm.contact-roles.lock
    where_checked: api
    scenario: ATS-CRM-ENT-015-16
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/planning/capabilities/CAP-CRM-ENT-015-lock-records-against-change-and-protect-the-records-rhythm.md
  - key: crm.contact-roles.unlock
    where_checked: api
    scenario: ATS-CRM-ENT-015-16
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/planning/capabilities/CAP-CRM-ENT-015-lock-records-against-change-and-protect-the-records-rhythm.md
  - key: crm.contacts.read
    where_checked: api
    scenario: ATS-CRM-ENT-015-16
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/planning/capabilities/CAP-CRM-ENT-015-lock-records-against-change-and-protect-the-records-rhythm.md
  - key: crm.organizations.read
    where_checked: api
    scenario: ATS-CRM-ENT-015-16
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/planning/capabilities/CAP-CRM-ENT-015-lock-records-against-change-and-protect-the-records-rhythm.md
  - key: crm.contact-roles.read
    where_checked: api
    scenario: ATS-CRM-ENT-015-16
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/planning/capabilities/CAP-CRM-ENT-015-lock-records-against-change-and-protect-the-records-rhythm.md
related_coverage:
  - capability: CAP-CRM-ENT-004
    scenarios:
      - ATS-CRM-ENT-015-10
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/planning/capabilities/CAP-CRM-ENT-015-lock-records-against-change-and-protect-the-records-rhythm.md:related
  - capability: CAP-CRM-ENT-014
    scenarios:
      - ATS-CRM-ENT-015-11
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/planning/capabilities/CAP-CRM-ENT-015-lock-records-against-change-and-protect-the-records-rhythm.md:related
  - capability: CAP-CRM-ENT-016
    scenarios:
      - ATS-CRM-ENT-015-12
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/planning/capabilities/CAP-CRM-ENT-015-lock-records-against-change-and-protect-the-records-rhythm.md:related
  - capability: CAP-CRM-ENT-017
    scenarios:
      - ATS-CRM-ENT-015-13
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/planning/capabilities/CAP-CRM-ENT-015-lock-records-against-change-and-protect-the-records-rhythm.md:related
  - capability: CAP-CRM-ENT-018
    scenarios:
      - ATS-CRM-ENT-015-14
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/planning/capabilities/CAP-CRM-ENT-015-lock-records-against-change-and-protect-the-records-rhythm.md:related
examination_evidence:
  provenance: documented
  source: 'rhythm-v2-docs:specs/crm/entities/reports/EXAMINATION-2026-10-23.md'
  heading_conflict: FND-CRM-ENT-075
---

# Control record locks and system protection
## Coverage

The category map separates specified cases from recorded gaps. A proposed case describes proof to implement; a binding identifies the assertions supplied by an existing executable file. The permission and transition ledgers record the individual checks needed for the capability.

| Required rule | Scenario or finding |
|---|---|
| `BR-CAP-CRM-ENT-015-01` | `ATS-CRM-ENT-015-01`, `ATS-CRM-ENT-015-16` |
| `BR-CAP-CRM-ENT-015-02` | `ATS-CRM-ENT-015-02`, `ATS-CRM-ENT-015-09`, `ATS-CRM-ENT-015-15` |
| `BR-CAP-CRM-ENT-015-03` | `ATS-CRM-ENT-015-03`, `ATS-CRM-ENT-015-12`, `ATS-CRM-ENT-015-15` |
| `BR-CAP-CRM-ENT-015-04` | `ATS-CRM-ENT-015-04`, `ATS-CRM-ENT-015-13`, `ATS-CRM-ENT-015-16` |
| `BR-CAP-CRM-ENT-015-05` | `ATS-CRM-ENT-015-05`, `ATS-CRM-ENT-015-10`, `ATS-CRM-ENT-015-15` |
| `BR-CAP-CRM-ENT-015-06` | `ATS-CRM-ENT-015-06`, `ATS-CRM-ENT-015-11` |
| `BR-CAP-CRM-ENT-015-07` | `ATS-CRM-ENT-015-07`, `ATS-CRM-ENT-015-16` |
| `BR-CAP-CRM-ENT-015-08` | `ATS-CRM-ENT-015-08` |

## Interactions With Related Capabilities

The interaction scenarios exercise save corrections without overwriting another change; remove records and their owned information; inspect a record’s changes over time; configure affiliation types and contact roles; organize the workspace’s record classifications. They change one part of a prepared record and inspect the dependent outcome. Unbound interactions remain proposed rather than implying an existing end-to-end implementation.

## Source Evidence

The frontmatter pins the repositories and identifies the source anchors for each scenario. Bindings record inspected assertions, while unbound cases remain proposed. Source inspection supplies no execution result or product approval.
