---
id: ATS-CRM-ENT-001
scope: CAP-CRM-ENT-001
product_area: crm
module: entities
revision: 1
status: proposed
source_sha:
  services-crm: a8d1364f1dd44b723bb5f9acba12f983e9ec8d69
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
scenarios:
  - id: ATS-CRM-ENT-001-01
    title: a contact needs a first or a last name
    level: api
    proves:
      - BR-CAP-CRM-ENT-001-01
      - INV-DOM-CRM-ENT-001-01
    preconditions: Prepare a workspace and a caller with crm.contacts.create.
    steps: POST a contact with blank first and last names.
    expected: 400 naming first_name; no contact written.
    negative: true
    data_needs: No additional fixtures.
    bound_to:
      - TST-CRM-ENT-786
    provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#answers
      400 for a contact with neither a first nor a last name, naming the rule
    category: validation
    action_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#answers
        400 for a contact with neither a first nor a last name, naming the rule
    assertion_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#answers
        400 for a contact with neither a first nor a last name, naming the rule
    test_anchors:
      - >-
        services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#answers
        400 for a contact with neither a first nor a last name, naming the rule
      - >-
        services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#answers
        400 naming the field for a contact whose first and last names are blank
    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-001-02
    title: the computed names are built from the parts
    level: api
    proves:
      - BR-CAP-CRM-ENT-001-02
      - INV-DOM-CRM-ENT-001-02
    preconditions: No additional setup is required.
    steps: POST a contact with every name part.
    expected: 'Name, full_name and name_last_first as the rule builds them.'
    negative: false
    data_needs: Use a contact with all seven name parts.
    bound_to:
      - TST-CRM-ENT-786
    provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#builds
      the full name from every part in order, trimmed and singly spaced
    category: rule
    action_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#builds
        the full name from every part in order, trimmed and singly spaced
    assertion_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#builds
        the full name from every part in order, trimmed and singly spaced
    test_anchors:
      - >-
        services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#builds
        the full name from every part in order, trimmed and singly spaced
      - >-
        services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#accepts
        a contact with a last name only and builds every name from 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-001-03
    title: a computed name cannot be sent and is never parsed
    level: api
    proves:
      - BR-CAP-CRM-ENT-001-03
    preconditions: No additional setup is required.
    steps: >-
      POST a contact with full_name and no parts; POST one with a name beside
      parts.
    expected: 400 each time; no parts derived.
    negative: true
    data_needs: No additional fixtures.
    bound_to:
      - TST-CRM-ENT-786
    provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#answers
      400 for a full name sent instead of name parts, and never splits it into
      parts
    category: validation
    action_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#answers
        400 for a full name sent instead of name parts, and never splits it into
        parts
    assertion_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#answers
        400 for a full name sent instead of name parts, and never splits it into
        parts
    test_anchors:
      - >-
        services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#answers
        400 for a full name sent instead of name parts, and never splits it into
        parts
      - >-
        services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#answers
        400 for a name sent beside valid name parts
    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-001-04
    title: Names are tidied and the tidying is displayed before saving
    level: browser
    proves:
      - BR-CAP-CRM-ENT-001-04
    preconditions: No additional setup is required.
    steps: 'Save salutation "mr", prefix "dr" and nickname equal to the first name.'
    expected: >-
      Prefix "Mr."/"Dr.", no salutation, no nickname; the console review lists
      both changes.
    negative: false
    data_needs: No additional fixtures.
    bound_to:
      - TST-CRM-ENT-787
      - TST-CRM-ENT-774
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#moves
      an honorific salutation to the prefix, start-cased and dotted (RB-2367)
    category: rule
    action_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#moves
        an honorific salutation to the prefix, start-cased and dotted (RB-2367)
    assertion_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#moves
        an honorific salutation to the prefix, start-cased and dotted (RB-2367)
    test_anchors:
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#moves
        an honorific salutation to the prefix, start-cased and dotted (RB-2367)
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#drops
        a nickname equal to the first name, ignoring case (RS-2647)
      - >-
        react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0003.spec.ts#CRM-ACC-0003
    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-001-05
    title: Field limits refuse overlong values naming each field
    level: browser
    proves:
      - BR-CAP-CRM-ENT-001-05
    preconditions: No additional setup is required.
    steps: >-
      Create and update with a 201-character first name, 10,001-character notes
      and a 255-character e-mail.
    expected: '400 naming each field; 200 and 10,000 accepted.'
    negative: true
    data_needs: No additional fixtures.
    bound_to:
      - TST-CRM-ENT-787
      - TST-CRM-ENT-774
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#names
      every overlong name part of one request at once
    category: boundary
    action_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#names
        every overlong name part of one request at once
    assertion_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#names
        every overlong name part of one request at once
    test_anchors:
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#refuses
        a first name of 201 characters, naming first_name
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#refuses
        notes of 10,001 characters, naming notes
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#refuses
        an e-mail address of 255 characters, naming the element’s email_address
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#refuses
        a limit on update as on create
      - >-
        react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0070.spec.ts#CRM-ACC-0070
    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-001-06
    title: 'Client ids are preserved, minted ids are UUIDv7, taken ids are rejected'
    level: api
    proves:
      - BR-CAP-CRM-ENT-001-06
    preconditions: No additional setup is required.
    steps: 'Create without an id, with a free id and with a taken id.'
    expected: UUIDv7; the named id; 400.
    negative: true
    data_needs: No additional fixtures.
    bound_to:
      - TST-CRM-ENT-786
      - TST-CRM-ENT-787
    provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#keeps the id
      the body named, which is no UUIDv7
    category: boundary
    action_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#keeps the
        id the body named, which is no UUIDv7
    assertion_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#keeps the
        id the body named, which is no UUIDv7
    test_anchors:
      - >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#gives the
        contact a UUIDv7 id when the body names none (Andrew, 2026-10-10)
      - >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#keeps the
        id the body named, which is no UUIDv7
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
        an id the tenant already uses, by contacts_pk
    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-001-07
    title: 'Numbers are the server''s, per workspace, with gaps and collision recovery'
    level: api
    proves:
      - BR-CAP-CRM-ENT-001-07
      - INV-DOM-CRM-ENT-001-03
    preconditions: Prepare a seed that hands out a number another row holds.
    steps: 'Create contacts, one rolled back.'
    expected: >-
      Consecutive numbers per workspace; a rolled-back create leaves a gap; a
      taken number becomes the highest plus one.
    negative: false
    data_needs: Use seeded number_seeds.
    bound_to:
      - TST-CRM-ENT-787
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#saves
      the contact under the highest number plus one, 4001
    category: invariant
    action_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#saves
        the contact under the highest number plus one, 4001
    assertion_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#saves
        the contact under the highest number plus one, 4001
    test_anchors:
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#takes
        the tenant's next number from platform.number_seeds
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#keeps
        the number consumed when the transaction rolls back: a gap, never given
        back
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#saves
        the contact under the highest number plus one, 4001
    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-001-08
    title: a retried create returns the first contact
    level: api
    proves:
      - BR-CAP-CRM-ENT-001-08
    preconditions: Prepare a create that committed.
    steps: >-
      Repeat it with the same id and body; then with the same id from another
      request.
    expected: 'The same contact, no second one; 409 for the other request.'
    negative: true
    data_needs: No additional fixtures.
    bound_to:
      - TST-CRM-ENT-787
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/contact-create.integration.spec.js#answers
      the retry with the contact the first attempt made
    category: edge
    action_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/integration/services/contacts/contact-create.integration.spec.js#answers
        the retry with the contact the first attempt made
    assertion_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/integration/services/contacts/contact-create.integration.spec.js#answers
        the retry with the contact the first attempt made
    test_anchors:
      - >-
        services-crm:modules/entities/tests/integration/services/contacts/contact-create.integration.spec.js#answers
        the retry with the contact the first attempt made
      - >-
        services-crm:modules/entities/tests/integration/services/contacts/contact-create.integration.spec.js#refuses
        it as a conflict (409), naming the id
    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-001-09
    title: the type is optional and must be the workspace’s
    level: api
    proves:
      - BR-CAP-CRM-ENT-001-09
      - INV-DOM-CRM-ENT-001-14
    preconditions: Prepare a workspace with one contact type.
    steps: 'Create with that type, with none and with an unknown one.'
    expected: Saved; saved with no type; 409 and nothing saved.
    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
      for a type the tenant does not have, and saves no contact
    category: validation
    action_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409
        for a type the tenant does not have, and saves no contact
    assertion_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409
        for a type the tenant does not have, and saves no contact
    test_anchors:
      - >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers the
        create with the type the body named
      - >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409
        for a type the tenant does not have, and saves no contact
    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-001-10
    title: Status is a closed list
    level: api
    proves:
      - BR-CAP-CRM-ENT-001-10
      - INV-DOM-CRM-ENT-001-04
    preconditions: No additional setup is required.
    steps: Write a contact with status "archived".
    expected: Rejected by contacts_status_check.
    negative: true
    data_needs: No additional fixtures.
    bound_to:
      - TST-CRM-ENT-787
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/db/contacts.table.integration.spec.js#refuses
      a status outside the closed list by contacts_status_check
    category: validation
    action_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/integration/db/contacts.table.integration.spec.js#refuses
        a status outside the closed list by contacts_status_check
    assertion_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/integration/db/contacts.table.integration.spec.js#refuses
        a status outside the closed list by contacts_status_check
    test_anchors:
      - >-
        services-crm:modules/entities/tests/integration/db/contacts.table.integration.spec.js#refuses
        a status outside the closed list by contacts_status_check
    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-001-11
    title: Spouse links are reciprocal and guarded
    level: api
    proves:
      - BR-CAP-CRM-ENT-001-11
      - INV-DOM-CRM-ENT-001-09
    preconditions: >-
      Three contacts exist; two are already a spouse pair. A separate pair is
      available for a valid link.
    steps: >-
      Create a reciprocal link in the free pair; try to claim a member of the
      existing pair; clear one side of the free pair; link it again and delete
      one spouse.
    expected: >-
      The free pair links in both directions. The occupied spouse is refused
      without changing either pair. Clearing or deleting a spouse clears the
      surviving reference.
    negative: true
    data_needs: No additional fixtures.
    bound_to:
      - TST-CRM-ENT-787
      - TST-CRM-ENT-786
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.spouse.integration.spec.js#links
      the other spouse back when a contact is created naming it
    category: invariant
    action_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/integration/services/persistence/contacts.spouse.integration.spec.js#links
        the other spouse back when a contact is created naming it
    assertion_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/integration/services/persistence/contacts.spouse.integration.spec.js#links
        the other spouse back when a contact is created naming it
    test_anchors:
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/contacts.spouse.integration.spec.js#links
        the other spouse back when a contact is created naming it
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/contacts.spouse.integration.spec.js#refuses
        to link a contact already recorded as the spouse of a third contact, and
        writes neither side
      - >-
        services-crm:modules/entities/tests/integration/services/persistence/contacts.spouse.integration.spec.js#clears
        the other spouse when one is deleted
      - >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409
        for a spouse the tenant does not have
    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-001-12
    title: Duplicate confirmation needs authorized override
    level: browser
    proves:
      - BR-CAP-CRM-ENT-001-12
    preconditions: Prepare a workspace whose e-mail match rule is set to block.
    steps: >-
      Attempt the blocked create as a non-holder with confirmation and a reason,
      then as an override holder with both.
    expected: 'The non-holder receives 409, and the override holder creates the contact.'
    negative: true
    data_needs: Use seeded duplicate rules.
    bound_to:
      - TST-CRM-ENT-774
    provenance: test
    source: >-
      react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0042.spec.ts#CRM-ACC-0042
    category: permission
    action_evidence:
      provenance: test
      source: >-
        react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0042.spec.ts#CRM-ACC-0042
    assertion_evidence:
      provenance: test
      source: >-
        react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0042.spec.ts#CRM-ACC-0042
    test_anchors:
      - >-
        react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0042.spec.ts#CRM-ACC-0042
    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-001-13
    title: a photo that cannot be saved rejects the create
    level: api
    proves:
      - BR-CAP-CRM-ENT-001-13
    preconditions: No additional setup is required.
    steps: POST a contact naming a staged upload that is not there.
    expected: 400 naming image_url and no contact.
    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 400
      for a photo whose upload is not there, naming the field that failed
    category: failure
    action_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400
        for a photo whose upload is not there, naming the field that failed
    assertion_evidence:
      provenance: test
      source: >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400
        for a photo whose upload is not there, naming the field that failed
    test_anchors:
      - >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400
        for a photo whose upload is not there, naming the field that failed
    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-001-14
    title: >-
      an override holder creates a blocked contact with a reason, and the
      override is recorded
    level: api
    proves:
      - BR-CAP-CRM-ENT-001-12
    preconditions: >-
      Prepare a caller holding crm.contacts.create-despite-duplicate-block; a
      blocking rule.
    steps: >-
      Create the blocked contact with duplicate_override_reason, then without a
      reason.
    expected: Created and an override row recorded; rejected without a reason.
    negative: false
    data_needs: Use seeded duplicate rules.
    bound_to: []
    provenance: proposed
    source: >-
      services-crm:modules/entities/services/contacts/contact-create.ts:createRequestedContact
    category: permission
    action_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/services/contacts/contact-create.ts:createRequestedContact
    assertion_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/services/contacts/contact-create.ts:createRequestedContact
    test_anchors: []
    binding_scope: 'unbound: the detailed scenario still needs executable proof.'
  - id: ATS-CRM-ENT-001-15
    title: 'the journey: a complete member recorded once and read the same everywhere'
    level: browser
    proves:
      - AS-JNY-CRM-ENT-001-01
      - AS-JNY-CRM-ENT-001-02
      - AS-JNY-CRM-ENT-001-03
      - AS-JNY-CRM-ENT-001-04
    preconditions: Prepare a member contact type and two custom fields; a standard user.
    steps: Run JNY-CRM-ENT-001.
    expected: >-
      One contact; the 360 and the API agree; the number is the server's; one
      revision per save.
    negative: false
    data_needs: Use seeded types and custom fields.
    bound_to:
      - TST-CRM-ENT-774
    provenance: test
    source: >-
      react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0001.spec.ts#CRM-ACC-0001
    category: interaction
    action_evidence:
      provenance: test
      source: >-
        react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0001.spec.ts#CRM-ACC-0001
    assertion_evidence:
      provenance: test
      source: >-
        react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0001.spec.ts#CRM-ACC-0001
    test_anchors:
      - >-
        react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0001.spec.ts#CRM-ACC-0001
      - >-
        react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0043.spec.ts#CRM-ACC-0043
    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-001-16
    title: Create each supported contact status
    category: transition
    level: api
    proves:
      - BR-CAP-CRM-ENT-001-10
    preconditions: Four valid person requests have distinct primary addresses.
    steps: Create one contact with each supported status and read every saved record.
    expected: >-
      Each saved status matches its request; the default-status request reads
      active.
    negative: false
    data_needs: Run-owned records and configuration in an isolated workspace.
    bound_to: []
    provenance: proposed
    source: >-
      services-crm:modules/entities/tests/integration/db/contacts.table.integration.spec.js#refuses
      a status outside the closed list by contacts_status_check
    test_anchors: []
    action_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/integration/db/contacts.table.integration.spec.js#refuses
        a status outside the closed list by contacts_status_check
    assertion_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/integration/db/contacts.table.integration.spec.js#refuses
        a status outside the closed list by contacts_status_check
    binding_scope: 'unbound: the detailed scenario still needs executable proof.'
  - id: ATS-CRM-ENT-001-17
    title: Race identical create retries
    category: concurrency
    level: api
    proves:
      - BR-CAP-CRM-ENT-001-08
    preconditions: >-
      Two clients share one unused lowercase time-ordered identity and identical
      person input.
    steps: >-
      Release both create requests together; read the identity and count records
      with that identity.
    expected: >-
      Both requests identify the same saved person and exactly one contact is
      present.
    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/contacts/contact-create.integration.spec.js#answers
      the retry with the contact the first attempt made
    test_anchors: []
    action_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/integration/services/contacts/contact-create.integration.spec.js#answers
        the retry with the contact the first attempt made
    assertion_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/integration/services/contacts/contact-create.integration.spec.js#answers
        the retry with the contact the first attempt made
    binding_scope: 'unbound: the detailed scenario still needs executable proof.'
  - id: ATS-CRM-ENT-001-18
    title: Find the newly established person
    category: interaction
    level: api
    proves:
      - BR-CAP-CRM-ENT-001-02
      - BR-CAP-CRM-ENT-003-06
    preconditions: A person request uses a distinctive last name.
    steps: 'Create the person, search by that name and open the saved record.'
    expected: >-
      Search identifies the saved person and the opened record carries the saved
      identity.
    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-names.e2e.spec.js#builds
      the full name from every part in order, trimmed and singly spaced;
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers only
      the fields `?fields=` named, with the id
    exercises:
      - CAP-CRM-ENT-001
      - CAP-CRM-ENT-003
    workflows: []
    test_anchors: []
    action_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#builds
        the full name from every part in order, trimmed and singly spaced;
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers
        only the fields `?fields=` named, with the id
    assertion_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#builds
        the full name from every part in order, trimmed and singly spaced;
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers
        only the fields `?fields=` named, with the id
    binding_scope: 'unbound: the detailed scenario still needs executable proof.'
  - id: ATS-CRM-ENT-001-19
    title: Correct a newly established person
    category: interaction
    level: api
    proves:
      - BR-CAP-CRM-ENT-001-02
      - BR-CAP-CRM-ENT-004-01
    preconditions: A person has just been created.
    steps: Read its version and correct its name parts through a version-tested save.
    expected: >-
      The corrected record retains its identity and shows the newly derived
      names.
    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-names.e2e.spec.js#builds
      the full name from every part in order, trimmed and singly spaced;
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400
      naming sys_version to a patch that tests no version
    exercises:
      - CAP-CRM-ENT-001
      - CAP-CRM-ENT-004
    workflows: []
    test_anchors: []
    action_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#builds
        the full name from every part in order, trimmed and singly spaced;
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400
        naming sys_version to a patch that tests no version
    assertion_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#builds
        the full name from every part in order, trimmed and singly spaced;
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400
        naming sys_version to a patch that tests no version
    binding_scope: 'unbound: the detailed scenario still needs executable proof.'
  - id: ATS-CRM-ENT-001-20
    title: Create a person with usable contact points
    category: interaction
    level: api
    proves:
      - BR-CAP-CRM-ENT-001-01
      - BR-CAP-CRM-ENT-005-01
    preconditions: A valid name and three distinct email values are prepared.
    steps: >-
      Create the person with all three addresses and read the ordered
      collection.
    expected: >-
      The contact is created once and the saved contact points retain their
      declared order.
    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-names.e2e.spec.js#answers
      400 for a contact with neither a first nor a last name, naming the rule;
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      a fourth e-mail address
    exercises:
      - CAP-CRM-ENT-001
      - CAP-CRM-ENT-005
    workflows:
      - WF-CRM-ENT-001
    test_anchors: []
    action_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#answers
        400 for a contact with neither a first nor a last name, naming the rule;
        services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
        a fourth e-mail address
    assertion_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#answers
        400 for a contact with neither a first nor a last name, naming the rule;
        services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
        a fourth e-mail address
    binding_scope: 'unbound: the detailed scenario still needs executable proof.'
  - id: ATS-CRM-ENT-001-21
    title: Create with a required customer value
    category: interaction
    level: api
    proves:
      - BR-CAP-CRM-ENT-001-01
      - BR-CAP-CRM-ENT-008-04
    preconditions: An active contact definition requires a value during creation.
    steps: >-
      Create without that value, then create with a valid value and read the
      resulting person.
    expected: >-
      The first attempt creates no person; the corrected attempt stores the
      person and its custom value together.
    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-names.e2e.spec.js#answers
      400 for a contact with neither a first nor a last name, naming the rule;
      services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#saves
      a replace of a contact created before the field was required, leaving the
      field out
    exercises:
      - CAP-CRM-ENT-001
      - CAP-CRM-ENT-008
    workflows:
      - WF-CRM-ENT-001
    test_anchors: []
    action_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#answers
        400 for a contact with neither a first nor a last name, naming the rule;
        services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#saves
        a replace of a contact created before the field was required, leaving
        the field out
    assertion_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#answers
        400 for a contact with neither a first nor a last name, naming the rule;
        services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#saves
        a replace of a contact created before the field was required, leaving
        the field out
    binding_scope: 'unbound: the detailed scenario still needs executable proof.'
  - id: ATS-CRM-ENT-001-22
    title: Keep the classification chosen before deactivation
    category: interaction
    level: api
    proves:
      - BR-CAP-CRM-ENT-001-09
      - BR-CAP-CRM-ENT-018-08
    preconditions: A coordinator has selected an active person classification.
    steps: >-
      Deactivate the classification before creation completes; save the person
      and reopen the chooser.
    expected: >-
      The saved person retains that classification while a new selection
      excludes the inactive choice.
    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 409
      for a type the tenant does not have, and saves no contact;
      react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0033.spec.ts#CRM-ACC-0033
    exercises:
      - CAP-CRM-ENT-001
      - CAP-CRM-ENT-018
    workflows: []
    test_anchors: []
    action_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409
        for a type the tenant does not have, and saves no contact;
        react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0033.spec.ts#CRM-ACC-0033
    assertion_evidence:
      provenance: proposed
      source: >-
        services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409
        for a type the tenant does not have, and saves no contact;
        react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0033.spec.ts#CRM-ACC-0033
    binding_scope: 'unbound: the detailed scenario still needs executable proof.'
  - id: ATS-CRM-ENT-001-23
    title: Exercise every declared authority independently
    category: permission
    level: api
    proves:
      - BR-CAP-CRM-ENT-003-07
      - BR-CAP-CRM-ENT-003-09
    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.create
      - crm.contacts.create-despite-duplicate-block
      - crm.contact-types.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/e2e/contacts-names.e2e.spec.js#answers
    400 for a contact with neither a first nor a last name, naming the rule
  - >-
    services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#answers
    400 naming the field for a contact whose first and last names are blank
  - >-
    services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#builds
    the full name from every part in order, trimmed and singly spaced
  - >-
    services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#accepts a
    contact with a last name only and builds every name from it
  - >-
    services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#answers
    400 for a full name sent instead of name parts, and never splits it into
    parts
  - >-
    services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#answers
    400 for a name sent beside valid name parts
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#moves
    an honorific salutation to the prefix, start-cased and dotted (RB-2367)
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#drops
    a nickname equal to the first name, ignoring case (RS-2647)
  - >-
    react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0003.spec.ts#CRM-ACC-0003
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#names
    every overlong name part of one request at once
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#refuses
    a first name of 201 characters, naming first_name
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#refuses
    notes of 10,001 characters, naming notes
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#refuses
    an e-mail address of 255 characters, naming the element’s email_address
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#refuses
    a limit on update as on create
  - >-
    react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0070.spec.ts#CRM-ACC-0070
  - >-
    services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#keeps the id
    the body named, which is no UUIDv7
  - >-
    services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#gives the
    contact a UUIDv7 id when the body names none (Andrew, 2026-10-10)
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
    an id the tenant already uses, by contacts_pk
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#saves
    the contact under the highest number plus one, 4001
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#takes
    the tenant's next number from platform.number_seeds
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#keeps
    the number consumed when the transaction rolls back: a gap, never given back
  - >-
    services-crm:modules/entities/tests/integration/services/contacts/contact-create.integration.spec.js#answers
    the retry with the contact the first attempt made
  - >-
    services-crm:modules/entities/tests/integration/services/contacts/contact-create.integration.spec.js#refuses
    it as a conflict (409), naming the id
  - >-
    services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409 for
    a type the tenant does not have, and saves no contact
  - >-
    services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers the
    create with the type the body named
  - >-
    services-crm:modules/entities/tests/integration/db/contacts.table.integration.spec.js#refuses
    a status outside the closed list by contacts_status_check
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/contacts.spouse.integration.spec.js#links
    the other spouse back when a contact is created naming it
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/contacts.spouse.integration.spec.js#refuses
    to link a contact already recorded as the spouse of a third contact, and
    writes neither side
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/contacts.spouse.integration.spec.js#clears
    the other spouse when one is deleted
  - >-
    services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409 for
    a spouse the tenant does not have
  - >-
    react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0042.spec.ts#CRM-ACC-0042
  - >-
    services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400 for
    a photo whose upload is not there, naming the field that failed
  - >-
    services-crm:modules/entities/services/contacts/contact-create.ts:createRequestedContact
  - >-
    react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0001.spec.ts#CRM-ACC-0001
  - >-
    react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0043.spec.ts#CRM-ACC-0043
  - >-
    services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#builds
    the full name from every part in order, trimmed and singly spaced;
    services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers only
    the fields `?fields=` named, with the id
  - >-
    services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#builds
    the full name from every part in order, trimmed and singly spaced;
    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-names.e2e.spec.js#answers
    400 for a contact with neither a first nor a last name, naming the rule;
    services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
    a fourth e-mail address
  - >-
    services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#answers
    400 for a contact with neither a first nor a last name, naming the rule;
    services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#saves
    a replace of a contact created before the field was required, leaving the
    field out
  - >-
    services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409 for
    a type the tenant does not have, and saves no contact;
    react-console:acceptance/crm/playwright/journeys/contacts/CRM-ACC-0033.spec.ts#CRM-ACC-0033
  - >-
    services-crm:aws/crm.service.ts:ApplicationPermissions;
    services-crm:modules/entities/aws/api/entities.routes.ts
coverage:
  rule: covered
  transition: covered
  validation: covered
  permission: covered
  edge: covered
  failure: covered
  boundary: covered
  concurrency: covered
  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-001
    from: new
    to: active
    scenarios:
      - ATS-CRM-ENT-001-16
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/design/state-models/STATE-CRM-ENT-001-contact-record-lifecycle.md:transitions
  - state_model: STATE-CRM-ENT-001
    from: new
    to: inactive
    scenarios:
      - ATS-CRM-ENT-001-16
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/design/state-models/STATE-CRM-ENT-001-contact-record-lifecycle.md:transitions
  - state_model: STATE-CRM-ENT-001
    from: new
    to: deceased
    scenarios:
      - ATS-CRM-ENT-001-16
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/design/state-models/STATE-CRM-ENT-001-contact-record-lifecycle.md:transitions
  - state_model: STATE-CRM-ENT-001
    from: new
    to: duplicate
    scenarios:
      - ATS-CRM-ENT-001-16
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/design/state-models/STATE-CRM-ENT-001-contact-record-lifecycle.md:transitions
coverage_notes: {}
coverage_findings:
  - FND-CRM-ENT-071
  - FND-CRM-ENT-075
permission_coverage:
  - key: crm.contacts.create
    where_checked: api
    scenario: ATS-CRM-ENT-001-23
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/planning/capabilities/CAP-CRM-ENT-001-record-a-person.md
  - key: crm.contacts.create-despite-duplicate-block
    where_checked: api
    scenario: ATS-CRM-ENT-001-23
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/planning/capabilities/CAP-CRM-ENT-001-record-a-person.md
  - key: crm.contact-types.read
    where_checked: api
    scenario: ATS-CRM-ENT-001-23
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/planning/capabilities/CAP-CRM-ENT-001-record-a-person.md
related_coverage:
  - capability: CAP-CRM-ENT-003
    scenarios:
      - ATS-CRM-ENT-001-18
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/planning/capabilities/CAP-CRM-ENT-001-record-a-person.md:related
  - capability: CAP-CRM-ENT-004
    scenarios:
      - ATS-CRM-ENT-001-19
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/planning/capabilities/CAP-CRM-ENT-001-record-a-person.md:related
  - capability: CAP-CRM-ENT-005
    scenarios:
      - ATS-CRM-ENT-001-20
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/planning/capabilities/CAP-CRM-ENT-001-record-a-person.md:related
  - capability: CAP-CRM-ENT-018
    scenarios:
      - ATS-CRM-ENT-001-22
    provenance: documented
    source: >-
      rhythm-v2-docs:specs/crm/entities/planning/capabilities/CAP-CRM-ENT-001-record-a-person.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
---

# Establish a person’s CRM record
## 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-001-01` | `ATS-CRM-ENT-001-01`, `ATS-CRM-ENT-001-20`, `ATS-CRM-ENT-001-21` |
| `BR-CAP-CRM-ENT-001-02` | `ATS-CRM-ENT-001-02`, `ATS-CRM-ENT-001-18`, `ATS-CRM-ENT-001-19` |
| `BR-CAP-CRM-ENT-001-03` | `ATS-CRM-ENT-001-03` |
| `BR-CAP-CRM-ENT-001-04` | `ATS-CRM-ENT-001-04` |
| `BR-CAP-CRM-ENT-001-05` | `ATS-CRM-ENT-001-05` |
| `BR-CAP-CRM-ENT-001-06` | `ATS-CRM-ENT-001-06` |
| `BR-CAP-CRM-ENT-001-07` | `ATS-CRM-ENT-001-07` |
| `BR-CAP-CRM-ENT-001-08` | `ATS-CRM-ENT-001-08`, `ATS-CRM-ENT-001-17` |
| `BR-CAP-CRM-ENT-001-09` | `ATS-CRM-ENT-001-09`, `ATS-CRM-ENT-001-22` |
| `BR-CAP-CRM-ENT-001-10` | `ATS-CRM-ENT-001-10`, `ATS-CRM-ENT-001-16` |
| `BR-CAP-CRM-ENT-001-11` | `ATS-CRM-ENT-001-11` |
| `BR-CAP-CRM-ENT-001-12` | `ATS-CRM-ENT-001-12`, `ATS-CRM-ENT-001-14` |
| `BR-CAP-CRM-ENT-001-13` | `ATS-CRM-ENT-001-13` |

## Interactions With Related Capabilities

The interaction scenarios exercise locate people and organizations; save corrections without overwriting another change; maintain reliable contact points; 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.
