---
id: TST-CRM-ENT-786
title: Entities local API lifecycle
kind: api-lifecycle
product_area: crm
module: entities
revision: 1
status: draft
source_sha:
  services-crm: a8d1364f1dd44b723bb5f9acba12f983e9ec8d69
repository: services-crm
files:
  - modules/entities/tests/e2e/address-types.e2e.spec.js
  - modules/entities/tests/e2e/affiliation-date-reconciliation.e2e.spec.js
  - modules/entities/tests/e2e/affiliation-types.e2e.spec.js
  - modules/entities/tests/e2e/application-access-enforced.e2e.spec.js
  - modules/entities/tests/e2e/archive.e2e.spec.js
  - modules/entities/tests/e2e/contact-affiliations.e2e.spec.js
  - modules/entities/tests/e2e/contact-information-sources.e2e.spec.js
  - modules/entities/tests/e2e/contact-roles.e2e.spec.js
  - modules/entities/tests/e2e/contact-types.e2e.spec.js
  - modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js
  - modules/entities/tests/e2e/contacts-disconnected.e2e.spec.js
  - modules/entities/tests/e2e/contacts-history.e2e.spec.js
  - modules/entities/tests/e2e/contacts-names.e2e.spec.js
  - modules/entities/tests/e2e/contacts.e2e.spec.js
  - modules/entities/tests/e2e/display-links.e2e.spec.js
  - modules/entities/tests/e2e/email-previews.e2e.spec.js
  - modules/entities/tests/e2e/organization-compliance.e2e.spec.js
  - modules/entities/tests/e2e/organization-types.e2e.spec.js
  - modules/entities/tests/e2e/organizations-custom-fields.e2e.spec.js
  - modules/entities/tests/e2e/organizations-domain-name.e2e.spec.js
  - modules/entities/tests/e2e/organizations-history.e2e.spec.js
  - modules/entities/tests/e2e/organizations.e2e.spec.js
  - modules/entities/tests/e2e/page-layouts.e2e.spec.js
  - modules/entities/tests/e2e/phone-number-types.e2e.spec.js
  - modules/entities/tests/e2e/platform-scope.e2e.spec.js
implements:
  - VT-IMC-CRM-ENT-108-01
  - VT-IMC-CRM-ENT-162-01
  - VT-IMC-CRM-ENT-006-01
  - VT-IMC-CRM-ENT-007-01
  - VT-IMC-CRM-ENT-008-01
  - ATS-CRM-ENT-001-01
  - ATS-CRM-ENT-001-02
  - ATS-CRM-ENT-001-03
  - ATS-CRM-ENT-001-06
  - ATS-CRM-ENT-001-09
  - ATS-CRM-ENT-001-11
  - ATS-CRM-ENT-001-13
  - ATS-CRM-ENT-002-01
  - ATS-CRM-ENT-002-02
  - ATS-CRM-ENT-002-04
  - ATS-CRM-ENT-002-05
  - ATS-CRM-ENT-002-09
  - ATS-CRM-ENT-003-01
  - ATS-CRM-ENT-003-05
  - ATS-CRM-ENT-003-06
  - ATS-CRM-ENT-003-08
  - ATS-CRM-ENT-003-09
  - ATS-CRM-ENT-004-01
  - ATS-CRM-ENT-004-03
  - ATS-CRM-ENT-004-04
  - ATS-CRM-ENT-004-05
  - ATS-CRM-ENT-004-06
  - ATS-CRM-ENT-004-07
  - ATS-CRM-ENT-004-09
  - ATS-CRM-ENT-004-10
  - ATS-CRM-ENT-005-03
  - ATS-CRM-ENT-005-05
  - ATS-CRM-ENT-005-06
  - ATS-CRM-ENT-005-07
  - ATS-CRM-ENT-005-08
  - ATS-CRM-ENT-005-09
  - ATS-CRM-ENT-006-01
  - ATS-CRM-ENT-006-05
  - ATS-CRM-ENT-006-06
  - ATS-CRM-ENT-006-07
  - ATS-CRM-ENT-008-01
  - ATS-CRM-ENT-008-02
  - ATS-CRM-ENT-008-03
  - ATS-CRM-ENT-008-04
  - ATS-CRM-ENT-008-05
  - ATS-CRM-ENT-008-06
  - ATS-CRM-ENT-008-07
  - ATS-CRM-ENT-009-01
  - ATS-CRM-ENT-009-04
  - ATS-CRM-ENT-009-05
  - ATS-CRM-ENT-009-08
  - ATS-CRM-ENT-009-11
  - ATS-CRM-ENT-009-14
  - ATS-CRM-ENT-009-15
  - ATS-CRM-ENT-010-06
  - ATS-CRM-ENT-011-01
  - ATS-CRM-ENT-011-03
  - ATS-CRM-ENT-012-02
  - ATS-CRM-ENT-012-06
  - ATS-CRM-ENT-012-10
  - ATS-CRM-ENT-012-11
  - ATS-CRM-ENT-012-12
  - ATS-CRM-ENT-012-13
  - ATS-CRM-ENT-013-01
  - ATS-CRM-ENT-013-02
  - ATS-CRM-ENT-013-04
  - ATS-CRM-ENT-013-05
  - ATS-CRM-ENT-013-06
  - ATS-CRM-ENT-014-02
  - ATS-CRM-ENT-014-05
  - ATS-CRM-ENT-015-04
  - ATS-CRM-ENT-015-05
  - ATS-CRM-ENT-015-06
  - ATS-CRM-ENT-015-07
  - ATS-CRM-ENT-016-01
  - ATS-CRM-ENT-016-02
  - ATS-CRM-ENT-016-03
  - ATS-CRM-ENT-016-05
  - ATS-CRM-ENT-017-01
  - ATS-CRM-ENT-017-02
  - ATS-CRM-ENT-017-03
  - ATS-CRM-ENT-017-05
  - ATS-CRM-ENT-017-06
  - ATS-CRM-ENT-017-07
  - ATS-CRM-ENT-017-08
  - ATS-CRM-ENT-017-11
  - ATS-CRM-ENT-018-01
  - ATS-CRM-ENT-018-02
  - ATS-CRM-ENT-018-03
  - ATS-CRM-ENT-018-04
  - ATS-CRM-ENT-018-05
  - ATS-CRM-ENT-018-06
  - ATS-CRM-ENT-018-07
  - ATS-CRM-ENT-019-01
  - ATS-CRM-ENT-019-02
  - ATS-CRM-ENT-019-03
  - ATS-CRM-ENT-019-04
  - ATS-CRM-ENT-019-05
  - ATS-CRM-ENT-019-06
  - ATS-CRM-ENT-019-07
environment: local
implementation_state: implemented
latest_result: not-run
latest_run_at: null
latest_run_ref: null
conformance: not-verified
estimated_minutes: null
estimate_basis: >-
  No measured duration is available for this cohesive group; per-file allowances
  are not runtime estimates.
isolation: >-
  The shared stages generate a test signing key, prepare valid route contexts
  and create scope-owned records. Teardown removes dependents and restores
  injected storage or communication adapters. A failed stage propagates its
  cause rather than letting missing data masquerade as a valid 404 or permission
  refusal.
binding_scope: >-
  Bindings preserve the existing assertion contributions and exact source file.
  Composite scenarios still require every bound contribution and any unbound
  checks; consolidation does not certify conformance.
bindings:
  - scenario: ATS-CRM-ENT-005-07
    source: >-
      services-crm:modules/entities/tests/e2e/address-types.e2e.spec.js#refuses
      a contact address of a type marked for organizations (400), naming the
      type field (CRM-M1 decision M1-D8)
  - scenario: ATS-CRM-ENT-018-01
    source: >-
      services-crm:modules/entities/tests/e2e/address-types.e2e.spec.js#answers
      400 without a name
  - scenario: ATS-CRM-ENT-018-02
    source: >-
      services-crm:modules/entities/tests/e2e/address-types.e2e.spec.js#answers
      400 for an id the API keeps for itself
  - scenario: ATS-CRM-ENT-018-03
    source: >-
      services-crm:modules/entities/tests/e2e/address-types.e2e.spec.js#refuses
      the delete of a type an address has (409)
  - scenario: ATS-CRM-ENT-018-03
    source: >-
      services-crm:modules/entities/tests/e2e/address-types.e2e.spec.js#says
      which records keep the type when it refuses the delete
  - scenario: ATS-CRM-ENT-018-04
    source: >-
      services-crm:modules/entities/tests/e2e/address-types.e2e.spec.js#puts a
      new type at the end of the tenant’s order when the body names no place
  - scenario: ATS-CRM-ENT-018-05
    source: >-
      services-crm:modules/entities/tests/e2e/address-types.e2e.spec.js#takes
      the type being marked for organizations while a contact address has it
      (200)
  - scenario: ATS-CRM-ENT-018-05
    source: >-
      services-crm:modules/entities/tests/e2e/address-types.e2e.spec.js#still
      answers the contact its address of that type
  - scenario: ATS-CRM-ENT-018-06
    source: >-
      services-crm:modules/entities/tests/e2e/address-types.e2e.spec.js#answers
      403 to a read-only caller’s reorder
  - scenario: ATS-CRM-ENT-018-06
    source: >-
      services-crm:modules/entities/tests/e2e/address-types.e2e.spec.js#lets a
      caller who may only read address types read them
  - scenario: ATS-CRM-ENT-010-06
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-date-reconciliation.e2e.spec.js#refuses
      a caller who may only read contacts, 403
  - scenario: ATS-CRM-ENT-010-06
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-date-reconciliation.e2e.spec.js#refuses
      a caller who may update contacts but not run bulk work, 403
  - scenario: ATS-CRM-ENT-010-06
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-date-reconciliation.e2e.spec.js#answers
      a dry run to the holder of the bulk key, with the window and nothing
      changed
  - scenario: ATS-CRM-ENT-010-06
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-date-reconciliation.e2e.spec.js#refuses
      a real run that names a day, 400 naming it
  - scenario: ATS-CRM-ENT-010-06
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-date-reconciliation.e2e.spec.js#refuses
      a day that is not a calendar date, 400 naming it
  - scenario: ATS-CRM-ENT-010-06
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-date-reconciliation.e2e.spec.js#refuses
      a property it does not know, 400 naming it
  - scenario: ATS-CRM-ENT-017-01
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#answers
      400 naming the field for a create whose name is nothing but spaces
  - scenario: ATS-CRM-ENT-017-02
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#derives
      the id from the name when the create names none
  - scenario: ATS-CRM-ENT-017-02
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#answers
      400 for a reserved id
  - scenario: ATS-CRM-ENT-017-02
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#answers
      400 for an id the tenant already uses
  - scenario: ATS-CRM-ENT-017-02
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#then,
      when the id is wrong, refuses the change (400)
  - scenario: ATS-CRM-ENT-017-05
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#refuses
      the delete (409) while an affiliation of a contact is of that type
  - scenario: ATS-CRM-ENT-017-05
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#leaves
      the type in place after the refusal
  - scenario: ATS-CRM-ENT-017-05
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#accepts
      the delete once the contact affiliated under it has gone
  - scenario: ATS-CRM-ENT-017-05
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#refuses
      the delete of the type that is the default (409)
  - scenario: ATS-CRM-ENT-017-06
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#answers
      other as the default of a tenant that chose none
  - scenario: ATS-CRM-ENT-017-06
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#saves
      a type of the tenant as the default and answers it
  - scenario: ATS-CRM-ENT-017-06
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#refuses
      a default the tenant does not have (400), naming the field
  - scenario: ATS-CRM-ENT-017-06
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#refuses
      a caller who may only read affiliation types the change of the default
      (403)
  - scenario: ATS-CRM-ENT-017-07
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#answers
      400 naming the field for a kind that is neither address nor phone
  - scenario: ATS-CRM-ENT-017-07
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#answers
      400 naming the field for a type id the tenant does not have
  - scenario: ATS-CRM-ENT-017-07
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#answers
      400 naming the field for a second mapping of one kind to one destination
  - scenario: ATS-CRM-ENT-017-07
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#answers
      400 naming the field for more than 20 mappings
  - scenario: ATS-CRM-ENT-017-07
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#clears
      the mappings for a replacement that sends null, as a later read shows
  - scenario: ATS-CRM-ENT-003-08
    source: >-
      services-crm:modules/entities/tests/e2e/application-access-enforced.e2e.spec.js#is
      refused where the assignment DENIES crm.application.access, though the
      route itself is served
  - scenario: ATS-CRM-ENT-003-08
    source: >-
      services-crm:modules/entities/tests/e2e/application-access-enforced.e2e.spec.js#is
      refused where the grants simply never name the access key, as a custom
      role written without it leaves them
  - scenario: ATS-CRM-ENT-003-08
    source: >-
      services-crm:modules/entities/tests/e2e/application-access-enforced.e2e.spec.js#is
      served it once the access key is allowed beside the same read key
  - scenario: ATS-CRM-ENT-013-01
    source: >-
      services-crm:modules/entities/tests/e2e/archive.e2e.spec.js#refuses a
      caller who may update contacts but not archive them, 403
  - scenario: ATS-CRM-ENT-013-01
    source: >-
      services-crm:modules/entities/tests/e2e/archive.e2e.spec.js#refuses a
      caller who may only read contacts, 403
  - scenario: ATS-CRM-ENT-013-01
    source: >-
      services-crm:modules/entities/tests/e2e/archive.e2e.spec.js#refuses a
      caller who may update organizations but not archive them, 403
  - scenario: ATS-CRM-ENT-013-01
    source: >-
      services-crm:modules/entities/tests/e2e/archive.e2e.spec.js#refuses the
      unarchive to the same callers, 403
  - scenario: ATS-CRM-ENT-013-01
    source: >-
      services-crm:modules/entities/tests/e2e/archive.e2e.spec.js#left the
      contact live after the refusals
  - scenario: ATS-CRM-ENT-013-02
    source: >-
      services-crm:modules/entities/tests/e2e/archive.e2e.spec.js#refuses an
      archive with no version, 400, naming it
  - scenario: ATS-CRM-ENT-013-02
    source: >-
      services-crm:modules/entities/tests/e2e/archive.e2e.spec.js#refuses a
      stale version, 409
  - scenario: ATS-CRM-ENT-013-02
    source: >-
      services-crm:modules/entities/tests/e2e/archive.e2e.spec.js#answers 404
      for a contact the tenant has not
  - scenario: ATS-CRM-ENT-013-04
    source: >-
      services-crm:modules/entities/tests/e2e/archive.e2e.spec.js#is unarchived
      to the record it was, editable again
  - scenario: ATS-CRM-ENT-013-05
    source: >-
      services-crm:modules/entities/tests/e2e/archive.e2e.spec.js#refuses a
      replacement, 409 RecordArchived
  - scenario: ATS-CRM-ENT-013-05
    source: >-
      services-crm:modules/entities/tests/e2e/archive.e2e.spec.js#refuses a
      patch, 409 RecordArchived
  - scenario: ATS-CRM-ENT-013-05
    source: >-
      services-crm:modules/entities/tests/e2e/archive.e2e.spec.js#refuses a save
      of the organization, 409 RecordArchived
  - scenario: ATS-CRM-ENT-013-06
    source: >-
      services-crm:modules/entities/tests/e2e/archive.e2e.spec.js#leaves it out
      of the list
  - scenario: ATS-CRM-ENT-013-06
    source: >-
      services-crm:modules/entities/tests/e2e/archive.e2e.spec.js#lists it for
      archived=only, and only it
  - scenario: ATS-CRM-ENT-013-06
    source: >-
      services-crm:modules/entities/tests/e2e/archive.e2e.spec.js#lists it with
      the live ones for archived=include
  - scenario: ATS-CRM-ENT-013-06
    source: >-
      services-crm:modules/entities/tests/e2e/archive.e2e.spec.js#is still read
      by its id
  - scenario: ATS-CRM-ENT-009-01
    source: >-
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#answers
      400 for an organization the tenant does not have
  - scenario: ATS-CRM-ENT-009-01
    source: >-
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#saves
      no contact for a write it refused
  - scenario: ATS-CRM-ENT-009-04
    source: >-
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#unmarks
      the old primary, which stays open and current
  - scenario: ATS-CRM-ENT-009-04
    source: >-
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#answers
      400 for two affiliations newly marked primary
  - scenario: ATS-CRM-ENT-009-05
    source: >-
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#answers
      400 for a primary affiliation with an end date, naming the field
  - scenario: ATS-CRM-ENT-009-08
    source: >-
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#accepts
      two affiliations to the same organization of the same type in separate
      periods
  - scenario: ATS-CRM-ENT-009-11
    source: >-
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#makes
      one primary affiliation of the default type other, starting today,
      open-ended, with the contact’s job title
  - scenario: ATS-CRM-ENT-009-11
    source: >-
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#ends
      the previous primary affiliation yesterday by default ('end'), no longer
      primary and now past
  - scenario: ATS-CRM-ENT-009-11
    source: >-
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#keeps
      the previous affiliation open and current, no longer primary, with 'keep'
  - scenario: ATS-CRM-ENT-009-11
    source: >-
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#starts
      the new affiliation on an earlier effective date and ends the previous one
      the day before it
  - scenario: ATS-CRM-ENT-009-11
    source: >-
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#leaves
      no primary organization when the organization is cleared, and ends the
      previous one yesterday
  - scenario: ATS-CRM-ENT-009-11
    source: >-
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#removes
      a previous primary that began on the day it is ended before, as a stay of
      no days
  - scenario: ATS-CRM-ENT-009-11
    source: >-
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#answers
      400 for an effective date after today, naming the field
  - scenario: ATS-CRM-ENT-009-15
    source: >-
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#answers
      the affiliations effective today by default, in contact order
  - scenario: ATS-CRM-ENT-009-15
    source: >-
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#answers
      the ended affiliations for effective=past
  - scenario: ATS-CRM-ENT-009-15
    source: >-
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#answers
      the affiliations effective on the as_of date
  - scenario: ATS-CRM-ENT-009-15
    source: >-
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#answers
      only the affiliations holding the contact_role_id given
  - scenario: ATS-CRM-ENT-009-15
    source: >-
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#answers
      200 with no affiliations for an organization the tenant does not have
  - scenario: ATS-CRM-ENT-009-15
    source: >-
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#answers
      403 permission_missing to a caller who may read organizations but not
      contacts
  - scenario: ATS-CRM-ENT-011-01
    source: >-
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#answers
      400 for a role that starts before its affiliation, naming the field
  - scenario: ATS-CRM-ENT-011-03
    source: >-
      services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js#ends
      the open roles of the ended affiliation the same day
  - scenario: ATS-CRM-ENT-006-01
    source: >-
      services-crm:modules/entities/tests/e2e/contact-information-sources.e2e.spec.js#refuses
      400 a type Acme has no record of, naming the item
  - scenario: ATS-CRM-ENT-009-14
    source: >-
      services-crm:modules/entities/tests/e2e/contact-information-sources.e2e.spec.js#refuses
      400 a second contact naming the same affiliation id, saying another
      affiliation has it
  - scenario: ATS-CRM-ENT-008-05
    source: >-
      services-crm:modules/entities/tests/e2e/contact-roles.e2e.spec.js#refuses
      a create that carries custom_fields, as an unknown property
  - scenario: ATS-CRM-ENT-008-05
    source: >-
      services-crm:modules/entities/tests/e2e/contact-roles.e2e.spec.js#refuses
      a replacement that carries custom_fields
  - scenario: ATS-CRM-ENT-008-05
    source: >-
      services-crm:modules/entities/tests/e2e/contact-roles.e2e.spec.js#refuses
      a patch that adds custom_fields
  - scenario: ATS-CRM-ENT-017-01
    source: >-
      services-crm:modules/entities/tests/e2e/contact-roles.e2e.spec.js#answers
      400 naming the field for a create whose name is the empty string
  - scenario: ATS-CRM-ENT-017-01
    source: >-
      services-crm:modules/entities/tests/e2e/contact-roles.e2e.spec.js#answers
      400 naming the field for a create whose name is nothing but spaces
  - scenario: ATS-CRM-ENT-017-01
    source: >-
      services-crm:modules/entities/tests/e2e/contact-roles.e2e.spec.js#writes
      no role at all for a create it refused: the id reads back 404
  - scenario: ATS-CRM-ENT-017-01
    source: >-
      services-crm:modules/entities/tests/e2e/contact-roles.e2e.spec.js#takes a
      name with spaces around it and stores it trimmed
  - scenario: ATS-CRM-ENT-017-02
    source: >-
      services-crm:modules/entities/tests/e2e/contact-roles.e2e.spec.js#answers
      400 for an id with a space
  - scenario: ATS-CRM-ENT-017-02
    source: >-
      services-crm:modules/entities/tests/e2e/contact-roles.e2e.spec.js#creates
      a role with a 63-character id and a 255-character name (CRM-ACC-0173)
  - scenario: ATS-CRM-ENT-017-02
    source: >-
      services-crm:modules/entities/tests/e2e/contact-roles.e2e.spec.js#answers
      400 naming the id for a 64-character id, never a 500 without a message
      (CRM-ACC-0173)
  - scenario: ATS-CRM-ENT-017-03
    source: >-
      services-crm:modules/entities/tests/e2e/contact-roles.e2e.spec.js#deletes
      a role no contact holds
  - scenario: ATS-CRM-ENT-017-03
    source: >-
      services-crm:modules/entities/tests/e2e/contact-roles.e2e.spec.js#answers
      409 for a role a contact holds, with the legacy words
  - scenario: ATS-CRM-ENT-017-08
    source: >-
      services-crm:modules/entities/tests/e2e/contact-roles.e2e.spec.js#answers
      200 with every role of the tenant, in the order the body listed them,
      numbered 1..n
  - scenario: ATS-CRM-ENT-017-08
    source: >-
      services-crm:modules/entities/tests/e2e/contact-roles.e2e.spec.js#answers
      400 for a reorder that leaves a role of the tenant out, naming it
  - scenario: ATS-CRM-ENT-017-08
    source: >-
      services-crm:modules/entities/tests/e2e/contact-roles.e2e.spec.js#answers
      400 for a reorder that names a role the tenant does not have
  - scenario: ATS-CRM-ENT-017-08
    source: >-
      services-crm:modules/entities/tests/e2e/contact-roles.e2e.spec.js#answers
      403 to a caller who may only read contact roles
  - scenario: ATS-CRM-ENT-017-08
    source: >-
      services-crm:modules/entities/tests/e2e/contact-roles.e2e.spec.js#leaves
      the order as the successful reorder left it after every refusal
  - scenario: ATS-CRM-ENT-017-11
    source: >-
      services-crm:modules/entities/tests/e2e/contact-roles.e2e.spec.js#answers
      a portal user of the tenant
  - scenario: ATS-CRM-ENT-018-01
    source: >-
      services-crm:modules/entities/tests/e2e/contact-types.e2e.spec.js#answers
      400 naming the field for a create whose name is the empty string
  - scenario: ATS-CRM-ENT-018-01
    source: >-
      services-crm:modules/entities/tests/e2e/contact-types.e2e.spec.js#answers
      400 naming the field for a create whose name is nothing but spaces
  - scenario: ATS-CRM-ENT-018-01
    source: >-
      services-crm:modules/entities/tests/e2e/contact-types.e2e.spec.js#writes
      no type at all for a create it refused: the id reads back 404
  - scenario: ATS-CRM-ENT-018-02
    source: >-
      services-crm:modules/entities/tests/e2e/contact-types.e2e.spec.js#derives
      the id from the name when the create names none
  - scenario: ATS-CRM-ENT-018-02
    source: >-
      services-crm:modules/entities/tests/e2e/contact-types.e2e.spec.js#answers
      400 for an id that is not a configuration id, saying what an id may hold
  - scenario: ATS-CRM-ENT-018-02
    source: >-
      services-crm:modules/entities/tests/e2e/contact-types.e2e.spec.js#answers
      400 for a reserved id
  - scenario: ATS-CRM-ENT-018-02
    source: >-
      services-crm:modules/entities/tests/e2e/contact-types.e2e.spec.js#answers
      400 for an id the tenant already uses
  - scenario: ATS-CRM-ENT-018-02
    source: >-
      services-crm:modules/entities/tests/e2e/contact-types.e2e.spec.js#then,
      when the id is wrong, refuses the change (400)
  - scenario: ATS-CRM-ENT-018-03
    source: >-
      services-crm:modules/entities/tests/e2e/contact-types.e2e.spec.js#refuses
      the delete of a type a contact has (409)
  - scenario: ATS-CRM-ENT-018-04
    source: >-
      services-crm:modules/entities/tests/e2e/contact-types.e2e.spec.js#answers
      the types in the order the body listed them, numbered 1..n
  - scenario: ATS-CRM-ENT-018-04
    source: >-
      services-crm:modules/entities/tests/e2e/contact-types.e2e.spec.js#answers
      400 for a reorder that leaves a type of the tenant out
  - scenario: ATS-CRM-ENT-018-04
    source: >-
      services-crm:modules/entities/tests/e2e/contact-types.e2e.spec.js#answers
      400 for a reorder that names a type twice
  - scenario: ATS-CRM-ENT-018-04
    source: >-
      services-crm:modules/entities/tests/e2e/contact-types.e2e.spec.js#leaves
      the order as the successful reorder left it after every refusal
  - scenario: ATS-CRM-ENT-018-04
    source: >-
      services-crm:modules/entities/tests/e2e/contact-types.e2e.spec.js#lists
      the types of a tenant that has arranged nothing alphabetically, each
      holding no place
  - scenario: ATS-CRM-ENT-018-06
    source: >-
      services-crm:modules/entities/tests/e2e/contact-types.e2e.spec.js#answers
      403 to a caller who may only read contact types
  - scenario: ATS-CRM-ENT-018-07
    source: >-
      services-crm:modules/entities/tests/e2e/contact-types.e2e.spec.js#then,
      when the version is off, refuses the change (409)
  - scenario: ATS-CRM-ENT-018-07
    source: >-
      services-crm:modules/entities/tests/e2e/contact-types.e2e.spec.js#and
      leaves what the patch said nothing about as it was
  - scenario: ATS-CRM-ENT-018-07
    source: >-
      services-crm:modules/entities/tests/e2e/contact-types.e2e.spec.js#refuses
      changing a type locked against updates
  - scenario: ATS-CRM-ENT-018-07
    source: >-
      services-crm:modules/entities/tests/e2e/contact-types.e2e.spec.js#refuses
      deleting a type locked against deletes
  - scenario: ATS-CRM-ENT-004-04
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#leaves
      every custom field as it was when the map is omitted
  - scenario: ATS-CRM-ENT-004-07
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#refuses
      a replacement with an invalid custom value as a whole, its standard change
      included
  - scenario: ATS-CRM-ENT-008-01
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#leaves
      every custom field as it was when the map is omitted
  - scenario: ATS-CRM-ENT-008-01
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#leaves
      every custom field as it was for an empty map
  - scenario: ATS-CRM-ENT-008-01
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#sets
      the key it sends, clears the key sent null and the selection sent empty,
      and leaves the others
  - scenario: ATS-CRM-ENT-008-01
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#merges
      a whole-map replacement key by key, never clearing what it leaves out
  - scenario: ATS-CRM-ENT-008-01
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#refuses
      a patch that removes the whole map
  - scenario: ATS-CRM-ENT-008-01
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#refuses
      custom_fields: null on a replacement
  - scenario: ATS-CRM-ENT-008-02
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#refuses
      a key the object does not have, at its path
  - scenario: ATS-CRM-ENT-008-02
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#refuses
      another tenant's field as unknown in this tenant
  - scenario: ATS-CRM-ENT-008-02
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#refuses
      a write to an archived field
  - scenario: ATS-CRM-ENT-008-03
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#stores
      a string of digits sent for a whole-number field as the number
  - scenario: ATS-CRM-ENT-008-03
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#stores
      the string false sent for a yes/no field as false
  - scenario: ATS-CRM-ENT-008-03
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#refuses
      yes for a yes/no field: no truthiness
  - scenario: ATS-CRM-ENT-008-03
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#refuses
      a text longer than the field allows
  - scenario: ATS-CRM-ENT-008-03
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#refuses
      an archived option
  - scenario: ATS-CRM-ENT-008-03
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#refuses
      a date and time without an offset, which is ambiguous
  - scenario: ATS-CRM-ENT-008-03
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#refuses
      a reference to a record the tenant does not have
  - scenario: ATS-CRM-ENT-008-04
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#refuses
      a create that leaves it out, naming it at its path
  - scenario: ATS-CRM-ENT-008-04
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#refuses
      a create that sends it blank
  - scenario: ATS-CRM-ENT-008-04
    source: >-
      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
  - scenario: ATS-CRM-ENT-008-04
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#refuses
      a replace that sends it null
  - scenario: ATS-CRM-ENT-008-04
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#refuses
      a patch that removes it
  - scenario: ATS-CRM-ENT-008-06
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#refuses
      a replacement with an invalid custom value as a whole, its standard change
      included
  - scenario: ATS-CRM-ENT-008-07
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js#puts
      no custom field at the top level of the record
  - scenario: ATS-CRM-ENT-006-05
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-disconnected.e2e.spec.js#turns
      the work address into a disconnected row, not a copy
  - scenario: ATS-CRM-ENT-006-05
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-disconnected.e2e.spec.js#says
      where it came from, when and why the source went
  - scenario: ATS-CRM-ENT-006-05
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-disconnected.e2e.spec.js#says
      it needs verification and that its source is disconnected, so no client
      draws it as synced
  - scenario: ATS-CRM-ENT-006-06
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-disconnected.e2e.spec.js#takes
      the record read sent back as it is, the provenance staying the server’s
  - scenario: ATS-CRM-ENT-006-06
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-disconnected.e2e.spec.js#refuses
      a client that asks for a row to be disconnected, naming the field
  - scenario: ATS-CRM-ENT-006-07
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-disconnected.e2e.spec.js#makes
      it the contact’s own, with the value unchanged
  - scenario: ATS-CRM-ENT-006-07
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-disconnected.e2e.spec.js#links
      it to the organization’s address through the affiliation
  - scenario: ATS-CRM-ENT-006-07
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-disconnected.e2e.spec.js#refuses
      a request naming both the record and its type, or neither
  - scenario: ATS-CRM-ENT-006-07
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-disconnected.e2e.spec.js#deletes
      the row
  - scenario: ATS-CRM-ENT-006-07
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-disconnected.e2e.spec.js#is
      refused 409 on a row that is no longer disconnected
  - scenario: ATS-CRM-ENT-006-07
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-disconnected.e2e.spec.js#refuses
      a stale version, 409
  - scenario: ATS-CRM-ENT-006-07
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-disconnected.e2e.spec.js#refuses
      a request with no version, 400, naming it
  - scenario: ATS-CRM-ENT-014-02
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-history.e2e.spec.js#keeps
      a deleted contact's history readable, with the record marked deleted and
      when
  - scenario: ATS-CRM-ENT-016-02
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-history.e2e.spec.js#refuses
      a caller who may read the contact but was not given its history
  - scenario: ATS-CRM-ENT-016-02
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-history.e2e.spec.js#answers
      a caller holding only read and history, which is the console reader
  - scenario: ATS-CRM-ENT-016-02
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-history.e2e.spec.js#keeps
      a deleted contact's history readable, with the record marked deleted and
      when
  - scenario: ATS-CRM-ENT-016-03
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-history.e2e.spec.js#pages
      with a cursor that does not move when the record changes behind it
  - scenario: ATS-CRM-ENT-016-03
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-history.e2e.spec.js#filters
      the timeline by person, operation, source and date
  - scenario: ATS-CRM-ENT-016-03
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-history.e2e.spec.js#refuses
      a query parameter it does not know, rather than ignoring the filter
  - scenario: ATS-CRM-ENT-016-03
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-history.e2e.spec.js#refuses
      a limit above the maximum page size
  - scenario: ATS-CRM-ENT-016-05
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-history.e2e.spec.js#reads
      one revision in full: the section, the field, its label, its data type and
      both values
  - scenario: ATS-CRM-ENT-016-05
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-history.e2e.spec.js#shows
      the role removed and the role added, each under the name it has now
  - scenario: ATS-CRM-ENT-016-05
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-history.e2e.spec.js#lists
      the affiliation the contact was created with as added, named by its
      organization and type
  - scenario: ATS-CRM-ENT-016-05
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-history.e2e.spec.js#names
      a custom field by the tenant's own label, and its value by its option's
  - scenario: ATS-CRM-ENT-001-01
    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
  - scenario: ATS-CRM-ENT-001-01
    source: >-
      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
  - scenario: ATS-CRM-ENT-001-02
    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
  - scenario: ATS-CRM-ENT-001-02
    source: >-
      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
  - scenario: ATS-CRM-ENT-001-03
    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
  - scenario: ATS-CRM-ENT-001-03
    source: >-
      services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js#answers
      400 for a name sent beside valid name parts
  - scenario: ATS-CRM-ENT-001-06
    source: >-
      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)
  - scenario: ATS-CRM-ENT-001-06
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#keeps the id
      the body named, which is no UUIDv7
  - scenario: ATS-CRM-ENT-001-09
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers the
      create with the type the body named
  - scenario: ATS-CRM-ENT-001-09
    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
  - scenario: ATS-CRM-ENT-001-11
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409
      for a spouse the tenant does not have
  - scenario: ATS-CRM-ENT-001-13
    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
  - scenario: ATS-CRM-ENT-003-01
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers the
      contacts that follow the cursor the first page gave
  - scenario: ATS-CRM-ENT-003-01
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers no
      cursor on the last page
  - scenario: ATS-CRM-ENT-003-01
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400
      for a limit above the range
  - scenario: ATS-CRM-ENT-003-01
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400
      for a cursor it did not answer
  - scenario: ATS-CRM-ENT-003-05
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers only
      the contacts a picker may offer: not the inactive, not the deceased
  - scenario: ATS-CRM-ENT-003-05
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400
      for a selectable that is neither true nor false
  - scenario: ATS-CRM-ENT-003-06
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers the
      other tenant its own contact for that id, to a context that covers it
  - scenario: ATS-CRM-ENT-003-06
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400
      for a body that chooses its own tenant
  - scenario: ATS-CRM-ENT-004-01
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400
      for a replacement that names no version
  - scenario: ATS-CRM-ENT-004-01
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400
      naming sys_version to a patch that tests no version
  - scenario: ATS-CRM-ENT-004-01
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409
      to a second patch at the version the first one moved on from
  - scenario: ATS-CRM-ENT-004-01
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#keeps the
      first writer’s value
  - scenario: ATS-CRM-ENT-004-01
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409
      with the code record_changed and the latest change when a colleague saved
      first (M1-D6)
  - scenario: ATS-CRM-ENT-004-03
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#sets the
      field the patch replaced and bumps the version
  - scenario: ATS-CRM-ENT-004-03
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#edits the one
      element the patch pointed at
  - scenario: ATS-CRM-ENT-004-03
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#appends the
      element the patch added
  - scenario: ATS-CRM-ENT-004-03
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#removes the
      element the patch removed
  - scenario: ATS-CRM-ENT-004-03
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#clears a
      field a later patch removes
  - scenario: ATS-CRM-ENT-004-05
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#keeps the
      edited element under its id, with its own version bumped
  - scenario: ATS-CRM-ENT-004-05
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#removes the
      element the body left out and adds the one it named
  - scenario: ATS-CRM-ENT-004-06
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 200
      for a replacement that sends back everything a read answered, the computed
      names included (M1-D5)
  - scenario: ATS-CRM-ENT-004-06
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400
      naming the full name for a replacement that sends back another than the
      stored one (M1-D5)
  - scenario: ATS-CRM-ENT-004-06
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400
      naming origin_service for a replacement that alters it (M1-D5)
  - scenario: ATS-CRM-ENT-004-09
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 200
      for a body with a blank e-mail element, dropping the element
      (DEF-CRM-0013)
  - scenario: ATS-CRM-ENT-004-10
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 409
      naming the system, not a colleague, when the system cleared the spouse
      link while the contact was edited (M1-D6)
  - scenario: ATS-CRM-ENT-005-03
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers the
      phone numbers in the read order, the preferred one the first in the
      tenant’s order
  - scenario: ATS-CRM-ENT-005-06
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 400
      for an address latitude above 90, naming the field
  - scenario: ATS-CRM-ENT-005-08
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#refuses
      calendar dates with a 400 naming the field
  - scenario: ATS-CRM-ENT-005-09
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers the
      seasonal address in season when the contact marks it, with the workspace
      date
  - scenario: ATS-CRM-ENT-005-09
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers the
      preferred address in season when the contact has not marked the season
  - scenario: ATS-CRM-ENT-005-09
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers the
      preferred address out of season
  - scenario: ATS-CRM-ENT-014-02
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#leaves
      nothing to read: a later GET answers 404
  - scenario: ATS-CRM-ENT-014-02
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 404
      to a second delete of the same id
  - scenario: ATS-CRM-ENT-015-04
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 403
      naming sys_locked for a create that sets a lock (E-14)
  - scenario: ATS-CRM-ENT-015-04
    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)
  - scenario: ATS-CRM-ENT-015-04
    source: >-
      services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js#answers 403
      naming sys_locked for a patch that sets a lock (E-14)
  - scenario: ATS-CRM-ENT-015-05
    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
  - scenario: ATS-CRM-ENT-015-06
    source: >-
      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)
  - scenario: ATS-CRM-ENT-015-06
    source: >-
      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)
  - scenario: ATS-CRM-ENT-015-07
    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)
  - scenario: ATS-CRM-ENT-015-07
    source: >-
      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)
  - scenario: ATS-CRM-ENT-012-11
    source: >-
      services-crm:modules/entities/tests/e2e/email-previews.e2e.spec.js#admits
      a caller holding the administration key and both read keys
  - scenario: ATS-CRM-ENT-012-11
    source: >-
      services-crm:modules/entities/tests/e2e/email-previews.e2e.spec.js#answers
      403 to a caller who may administer types but may not read the records the
      answer contains
  - scenario: ATS-CRM-ENT-012-11
    source: >-
      services-crm:modules/entities/tests/e2e/email-previews.e2e.spec.js#answers
      400 for a contact that is not linked to the organization
  - scenario: ATS-CRM-ENT-012-11
    source: >-
      services-crm:modules/entities/tests/e2e/email-previews.e2e.spec.js#answers
      200 with the problem, because a template’s problems are an answer and not
      a refusal
  - scenario: ATS-CRM-ENT-012-02
    source: >-
      services-crm:modules/entities/tests/e2e/organization-compliance.e2e.spec.js#answer
      the computed status and violations of an organization created with a
      compliance type
  - scenario: ATS-CRM-ENT-012-06
    source: >-
      services-crm:modules/entities/tests/e2e/organization-compliance.e2e.spec.js#keep
      them against a PUT that sends others back
  - scenario: ATS-CRM-ENT-012-10
    source: >-
      services-crm:modules/entities/tests/e2e/organization-compliance.e2e.spec.js#answers
      403 to a caller who may only read organizations
  - scenario: ATS-CRM-ENT-012-12
    source: >-
      services-crm:modules/entities/tests/e2e/organization-compliance.e2e.spec.js#answers
      200 with Platform's answer to the one test send: sent, with its message id
  - scenario: ATS-CRM-ENT-012-12
    source: >-
      services-crm:modules/entities/tests/e2e/organization-compliance.e2e.spec.js#answers
      400 for a template both named and typed in, or neither
  - scenario: ATS-CRM-ENT-012-12
    source: >-
      services-crm:modules/entities/tests/e2e/organization-compliance.e2e.spec.js#answers
      400 for a typed-in template without a subject, naming the field
  - scenario: ATS-CRM-ENT-012-12
    source: >-
      services-crm:modules/entities/tests/e2e/organization-compliance.e2e.spec.js#answers
      404 for a contact the tenant does not have
  - scenario: ATS-CRM-ENT-002-04
    source: >-
      services-crm:modules/entities/tests/e2e/organization-types.e2e.spec.js#refuses
      an organization of a type the tenant does not have (409), and the delete
      of a type an organization has (409)
  - scenario: ATS-CRM-ENT-012-13
    source: >-
      services-crm:modules/entities/tests/e2e/organization-types.e2e.spec.js#runs
      the legacy cleaning on save: rules with neither bound dropped
  - scenario: ATS-CRM-ENT-012-13
    source: >-
      services-crm:modules/entities/tests/e2e/organization-types.e2e.spec.js#answers
      409 for a rule of a role the tenant does not have, and saves nothing
  - scenario: ATS-CRM-ENT-012-13
    source: >-
      services-crm:modules/entities/tests/e2e/organization-types.e2e.spec.js#answers
      400 when a switch is on and its list is missing, naming the field
  - scenario: ATS-CRM-ENT-018-01
    source: >-
      services-crm:modules/entities/tests/e2e/organization-types.e2e.spec.js#answers
      400 naming the field for a create whose name is nothing but spaces
  - scenario: ATS-CRM-ENT-018-03
    source: >-
      services-crm:modules/entities/tests/e2e/organization-types.e2e.spec.js#refuses
      an organization of a type the tenant does not have (409), and the delete
      of a type an organization has (409)
  - scenario: ATS-CRM-ENT-018-06
    source: >-
      services-crm:modules/entities/tests/e2e/organization-types.e2e.spec.js#answers
      403 to a caller who may only read organization types, and lets that caller
      read
  - scenario: ATS-CRM-ENT-018-07
    source: >-
      services-crm:modules/entities/tests/e2e/organization-types.e2e.spec.js#refuses
      changing a type locked against updates, and deleting one locked against
      deletes
  - scenario: ATS-CRM-ENT-008-01
    source: >-
      services-crm:modules/entities/tests/e2e/organizations-custom-fields.e2e.spec.js#refuses
      custom_fields: null
  - scenario: ATS-CRM-ENT-008-02
    source: >-
      services-crm:modules/entities/tests/e2e/organizations-custom-fields.e2e.spec.js#refuses
      a contact's custom field as unknown on an organization
  - scenario: ATS-CRM-ENT-008-03
    source: >-
      services-crm:modules/entities/tests/e2e/organizations-custom-fields.e2e.spec.js#refuses
      a number that cannot be written exactly for a percentage
  - scenario: ATS-CRM-ENT-008-04
    source: >-
      services-crm:modules/entities/tests/e2e/organizations-custom-fields.e2e.spec.js#refuses
      a patch that empties the selection
  - scenario: ATS-CRM-ENT-002-09
    source: >-
      services-crm:modules/entities/tests/e2e/organizations-domain-name.e2e.spec.js#answers
      200 with the domain name derived from the website
  - scenario: ATS-CRM-ENT-016-01
    source: >-
      services-crm:modules/entities/tests/e2e/organizations-history.e2e.spec.js#has
      one revision per committed save, newest first
  - scenario: ATS-CRM-ENT-016-02
    source: >-
      services-crm:modules/entities/tests/e2e/organizations-history.e2e.spec.js#answers
      a caller with the history key but no read of organizations 404, as the
      organization itself would
  - scenario: ATS-CRM-ENT-016-02
    source: >-
      services-crm:modules/entities/tests/e2e/organizations-history.e2e.spec.js#keeps
      its history, marked deleted
  - scenario: ATS-CRM-ENT-002-01
    source: >-
      services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js#answers
      400 for a body with no name
  - scenario: ATS-CRM-ENT-002-02
    source: >-
      services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js#keeps
      the id the body named, which is no UUIDv7
  - scenario: ATS-CRM-ENT-002-02
    source: >-
      services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js#gives
      the organization a UUIDv7 id when the body names none (Andrew, 2026-10-10)
  - scenario: ATS-CRM-ENT-002-02
    source: >-
      services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js#answers
      400 for an id that breaks the id grammar, naming the characters an id may
      hold
  - scenario: ATS-CRM-ENT-002-05
    source: >-
      services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js#answers
      409 for a parent the tenant does not have
  - scenario: ATS-CRM-ENT-003-01
    source: >-
      services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js#answers
      the rest of the tenant organizations after that cursor
  - scenario: ATS-CRM-ENT-004-01
    source: >-
      services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js#answers
      409 for a version the organization has moved past
  - scenario: ATS-CRM-ENT-004-03
    source: >-
      services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js#clears
      the field the patch removed
  - scenario: ATS-CRM-ENT-004-05
    source: >-
      services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js#keeps
      the edited element under its id, with its own version bumped
  - scenario: ATS-CRM-ENT-005-03
    source: >-
      services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js#answers
      400 for a phone number type the tenant does not have
  - scenario: ATS-CRM-ENT-005-05
    source: >-
      services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js#answers
      400 for a body with two addresses of one type
  - scenario: ATS-CRM-ENT-014-05
    source: >-
      services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js#answers
      409 for an organization that other organizations name as their parent
  - scenario: ATS-CRM-ENT-014-05
    source: >-
      services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js#answers
      409 to a delete while the contact is affiliated with it
  - scenario: ATS-CRM-ENT-014-05
    source: >-
      services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js#accepts
      the delete once the contact has gone
  - scenario: ATS-CRM-ENT-015-04
    source: >-
      services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js#answers
      403 naming sys_locked for a value that is no lock
  - scenario: ATS-CRM-ENT-015-07
    source: >-
      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
  - scenario: ATS-CRM-ENT-017-06
    source: >-
      services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js#is not
      answered the default affiliation type, which the affiliation types keys
      govern
  - scenario: ATS-CRM-ENT-019-01
    source: >-
      services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js#is
      answered the section it may read
  - scenario: ATS-CRM-ENT-019-01
    source: >-
      services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js#is not
      answered the section it may not read: it is ABSENT, not null, so
      "forbidden" and "unset" never look alike
  - scenario: ATS-CRM-ENT-019-01
    source: >-
      services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js#is not
      answered the default affiliation type, which the affiliation types keys
      govern
  - scenario: ATS-CRM-ENT-019-02
    source: >-
      services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js#may save
      its own section
  - scenario: ATS-CRM-ENT-019-02
    source: >-
      services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js#is
      refused (403) when it saves the other section
  - scenario: ATS-CRM-ENT-019-02
    source: >-
      services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js#is
      refused (403) for a body naming both sections, though it may change one of
      them
  - scenario: ATS-CRM-ENT-019-02
    source: >-
      services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js#writes
      NEITHER section for that refused save: the decision is made before
      anything is written
  - scenario: ATS-CRM-ENT-019-03
    source: >-
      services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js#saves a
      default and answers it, at version 1
  - scenario: ATS-CRM-ENT-019-03
    source: >-
      services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js#leaves a
      section a later save does not name
  - scenario: ATS-CRM-ENT-019-03
    source: >-
      services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js#refuses a
      save from a stale version (409)
  - scenario: ATS-CRM-ENT-019-03
    source: >-
      services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js#refuses
      an unknown property rather than ignoring it (400)
  - scenario: ATS-CRM-ENT-019-04
    source: >-
      services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js#refuses a
      definition of the organizations purpose on a contact type (400)
  - scenario: ATS-CRM-ENT-019-05
    source: >-
      services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js#KEEPS all
      five through a PUT that does not mention them: an older client cannot wipe
      a tenant’s assignments
  - scenario: ATS-CRM-ENT-019-05
    source: >-
      services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js#CLEARS
      the one a PATCH removes, and keeps the rest: the opposite of the same
      silence in a PUT
  - scenario: ATS-CRM-ENT-019-05
    source: >-
      services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js#clears
      one a PATCH replaces with null
  - scenario: ATS-CRM-ENT-019-06
    source: >-
      services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js#answers
      the type’s own definition for the kind the type assigns
  - scenario: ATS-CRM-ENT-019-06
    source: >-
      services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js#falls
      back to the tenant’s default for a kind the type leaves empty
  - scenario: ATS-CRM-ENT-019-06
    source: >-
      services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js#says a
      kind nothing configures is not configured
  - scenario: ATS-CRM-ENT-019-06
    source: >-
      services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js#answers
      only the console kinds for ?context=console
  - scenario: ATS-CRM-ENT-019-06
    source: >-
      services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js#refuses a
      type_id the tenant does not have (404) rather than answering the defaults
  - scenario: ATS-CRM-ENT-019-06
    source: >-
      services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js#refuses a
      request that names no entity (400)
  - scenario: ATS-CRM-ENT-019-07
    source: >-
      services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js#is shown
      the contact definitions: being told which layout is in force is part of
      reading the record
  - scenario: ATS-CRM-ENT-019-07
    source: >-
      services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js#is
      refused the organization ones (403): the key is the entity’s own
  - scenario: ATS-CRM-ENT-019-07
    source: >-
      services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js#is
      refused the tenant’s defaults (403): reading a record does not make one an
      administrator of layouts
  - scenario: ATS-CRM-ENT-005-07
    source: >-
      services-crm:modules/entities/tests/e2e/phone-number-types.e2e.spec.js#refuses
      a contact phone number of a type marked for organizations (400), naming
      the type field (CRM-M1 decision M1-D8)
  - scenario: ATS-CRM-ENT-018-01
    source: >-
      services-crm:modules/entities/tests/e2e/phone-number-types.e2e.spec.js#answers
      400 without a name
  - scenario: ATS-CRM-ENT-018-03
    source: >-
      services-crm:modules/entities/tests/e2e/phone-number-types.e2e.spec.js#refuses
      the delete of a type a number has (409)
  - scenario: ATS-CRM-ENT-018-05
    source: >-
      services-crm:modules/entities/tests/e2e/phone-number-types.e2e.spec.js#applies
      a type to contacts when the create says nothing
  - scenario: ATS-CRM-ENT-018-05
    source: >-
      services-crm:modules/entities/tests/e2e/phone-number-types.e2e.spec.js#answers
      only the types usable on organizations when the query asks for them
  - scenario: ATS-CRM-ENT-018-05
    source: >-
      services-crm:modules/entities/tests/e2e/phone-number-types.e2e.spec.js#answers
      a type of both in the lists of each kind
  - scenario: ATS-CRM-ENT-018-06
    source: >-
      services-crm:modules/entities/tests/e2e/phone-number-types.e2e.spec.js#answers
      403 to a read-only caller’s reorder
  - scenario: ATS-CRM-ENT-003-09
    source: >-
      services-crm:modules/entities/tests/e2e/platform-scope.e2e.spec.js#refuses
      every one of them to a staff slice reaching every tenant, 403 at the
      tenant boundary
evidence:
  - 'services-crm:modules/entities/tests/e2e/address-types.e2e.spec.js'
  - >-
    services-crm:modules/entities/tests/e2e/affiliation-date-reconciliation.e2e.spec.js
  - 'services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js'
  - >-
    services-crm:modules/entities/tests/e2e/application-access-enforced.e2e.spec.js
  - 'services-crm:modules/entities/tests/e2e/archive.e2e.spec.js'
  - 'services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js'
  - >-
    services-crm:modules/entities/tests/e2e/contact-information-sources.e2e.spec.js
  - 'services-crm:modules/entities/tests/e2e/contact-roles.e2e.spec.js'
  - 'services-crm:modules/entities/tests/e2e/contact-types.e2e.spec.js'
  - 'services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js'
  - 'services-crm:modules/entities/tests/e2e/contacts-disconnected.e2e.spec.js'
  - 'services-crm:modules/entities/tests/e2e/contacts-history.e2e.spec.js'
  - 'services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js'
  - 'services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js'
  - 'services-crm:modules/entities/tests/e2e/display-links.e2e.spec.js'
  - 'services-crm:modules/entities/tests/e2e/email-previews.e2e.spec.js'
  - 'services-crm:modules/entities/tests/e2e/organization-compliance.e2e.spec.js'
  - 'services-crm:modules/entities/tests/e2e/organization-types.e2e.spec.js'
  - >-
    services-crm:modules/entities/tests/e2e/organizations-custom-fields.e2e.spec.js
  - >-
    services-crm:modules/entities/tests/e2e/organizations-domain-name.e2e.spec.js
  - 'services-crm:modules/entities/tests/e2e/organizations-history.e2e.spec.js'
  - 'services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js'
  - 'services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js'
  - 'services-crm:modules/entities/tests/e2e/phone-number-types.e2e.spec.js'
  - 'services-crm:modules/entities/tests/e2e/platform-scope.e2e.spec.js'
source_observations:
  - provenance: test
    source: 'services-crm:modules/entities/tests/e2e/address-types.e2e.spec.js'
    declared_test_titles:
      - address types API (end to end)
      - 'the lifecycle (the shared lifecycle suite, step by step)'
      - >-
        is able to write a brand new record: 200 with the type as saved, version
        1, in the tenant of the path
      - >-
        puts a new type at the end of the tenant’s order when the body names no
        place
      - 'and is then able to read that record: every field as written'
      - and can get a list containing it
      - answers the list in the tenant’s display order
      - 'then updates the whole object, bumping the version'
      - >-
        and that replacement is what a later read answers: it was persisted, not
        just returned
      - 'then, when the version is off, refuses the change (409)'
      - 'then, when the id is wrong, refuses the change (400)'
      - then patches the object
      - and leaves what the patch said nothing about as it was
      - >-
        then can be deleted, and when read nothing comes back and a 404 is
        returned, as for a second delete
      - the schema rules
      - derives the id from the name when the create names none
      - answers 400 for an id that is not a configuration id
      - answers 400 for an id the API keeps for itself
      - applies a type to contacts when the create says nothing
      - answers 400 without a name
      - answers 400 for an applicability the contract does not carry
      - answers 400 for a property the contract does not carry
      - answers 400 for an id the tenant already uses
      - answers 404 for a type the tenant does not have
      - the list of the types a record may use
      - answers only the types usable on contacts when the query asks for them
      - >-
        answers the organization types the seed gave the tenant when the query
        asks for those
      - answers 400 for an applicability the list does not take
      - the order of the tenant’s types
      - answers 200 with every type of the tenant
      - 'answers the types in the order the body listed them, numbered 1..n'
      - answers what the list answers afterwards
      - >-
        persists the new place and bumps the version of a type it moved: a later
        read answers both
      - answers 400 for a reorder that names a type the tenant does not have
      - answers 400 for a reorder that leaves a type of the tenant out
      - answers 400 for a reorder that names a type twice
      - answers 400 for a body without the ids
      - leaves the order as the successful reorder left it after every refusal
      - a type a record uses
      - accepts a contact address of a type that applies to contacts
      - refuses the delete of a type an address has (409)
      - says which records keep the type when it refuses the delete
      - >-
        takes the type being marked for organizations while a contact address
        has it (200)
      - still answers the contact its address of that type
      - >-
        refuses a contact address of a type marked for organizations (400),
        naming the type field (CRM-M1 decision M1-D8)
      - locks and access
      - refuses changing a type locked against updates
      - refuses deleting a type locked against deletes
      - answers 401 without a context
      - answers 403 for a tenant the context does not cover
      - answers 403 to a write in a tenant the context does not cover
      - answers 403 to a caller who may only read address types
      - answers 403 to a read-only caller’s reorder
      - lets a caller who may only read address types read them
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies
      and transaction behavior are real dependencies. The harness supplies
      locally signed access contexts rather than a deployed authorizer.
  - provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-date-reconciliation.e2e.spec.js
    declared_test_titles:
      - the affiliation date reconciliation API (end to end)
      - 'refuses a caller who may only read contacts, 403'
      - 'refuses a caller who may update contacts but not run bulk work, 403'
      - >-
        answers a dry run to the holder of the bulk key, with the window and
        nothing changed
      - >-
        leaves the day open after a dry run: the real run that follows settles
        it
      - finds the day settled when asked again
      - takes no body at all as a real run
      - 'refuses a real run that names a day, 400 naming it'
      - 'refuses a day that is not a calendar date, 400 naming it'
      - 'refuses a property it does not know, 400 naming it'
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies
      and transaction behavior are real dependencies. The harness supplies
      locally signed access contexts rather than a deployed authorizer.
  - provenance: test
    source: 'services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js'
    declared_test_titles:
      - affiliation types API (end to end)
      - 'the lifecycle (the shared lifecycle suite, step by step)'
      - >-
        is able to write a brand new record: 200 with the type as saved, version
        1, in the tenant of the path
      - 'and is then able to read that record: every field as written'
      - and can get a list containing it
      - 'then updates the whole object, bumping the version'
      - >-
        and that replacement is what a later read answers: it was persisted, not
        just returned
      - 'then, when the version is off, refuses the change (409)'
      - 'then, when the id is wrong, refuses the change (400)'
      - then patches the object
      - and leaves what the patch said nothing about as it was
      - >-
        then can be deleted, and when read nothing comes back and a 404 is
        returned, as for a second delete
      - the schema rules
      - derives the id from the name when the create names none
      - >-
        answers 400 for an id that is not a configuration id, saying what an id
        may hold
      - answers 400 for a reserved id
      - answers 400 without a name
      - answers 400 for a property the contract does not carry
      - answers 400 for an id the tenant already uses
      - answers 404 for a type the tenant does not have
      - the name a type must have
      - answers 400 naming the field for a create whose name is the empty string
      - >-
        answers 400 naming the field for a create whose name is nothing but
        spaces
      - 'writes no type at all for a create it refused: the id reads back 404'
      - takes a name with spaces around it and stores it trimmed
      - a replacement and a patch that blank the name of a type that has one
      - 'refuses the replacement 400, naming the field'
      - leaves the record exactly as it was after that replacement
      - 'refuses the patch 400, naming the field'
      - leaves the record exactly as it was after that patch
      - the tenant’s default affiliation type
      - answers other as the default of a tenant that chose none
      - saves a type of the tenant as the default and answers it
      - >-
        answers the same default on a later read: it was persisted, not just
        returned
      - 'refuses a default the tenant does not have (400), naming the field'
      - refuses the delete of the type that is the default (409)
      - >-
        refuses a caller who may only read affiliation types the change of the
        default (403)
      - >-
        answers a caller who may only read affiliation types the default, and no
        layout section
      - a type a contact’s affiliation uses
      - >-
        refuses the delete (409) while an affiliation of a contact is of that
        type
      - leaves the type in place after the refusal
      - accepts the delete once the contact affiliated under it has gone
      - locks and access
      - refuses changing a type locked against updates
      - refuses deleting a type locked against deletes
      - answers 401 without a context
      - answers 403 for a tenant the context does not cover
      - answers 403 to a caller who may only read affiliation types
      - lets a caller who may only read affiliation types read them
      - 'contact_information_defaults, the default mappings on the record'
      - the lifecycle of a type with mappings
      - 'answers the created type with its mappings, addresses first'
      - answers the same record on a read
      - answers the mappings on the element of the list
      - 'replaces the list as a whole on a replacement, bumping the version'
      - 'adds a mapping with a patch, keeping the others'
      - >-
        clears the mappings for a replacement that sends null, as a later read
        shows
      - answers an empty list for a type created without mappings
      - >-
        answers 400 naming the field for a kind that is neither address nor
        phone
      - answers 400 naming the field for a type id the tenant does not have
      - >-
        answers 400 naming the field for a second mapping of one kind to one
        destination
      - answers 400 naming the field for more than 20 mappings
      - 'writes no type for a create it refused: the id reads back 404'
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies
      and transaction behavior are real dependencies. The harness supplies
      locally signed access contexts rather than a deployed authorizer.
  - provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/application-access-enforced.e2e.spec.js
    declared_test_titles:
      - >-
        the application access and licence steps as a deployed CRM function
        evaluates them
      - the words the functions are deployed with (premise)
      - >-
        evaluates the application access step in enforce, which is what every
        case below is about
      - evaluates the licence step in enforce too
      - >-
        a person whose assignment allows every CRM key and the access key with
        it
      - >-
        is served a CRM route, which is the premise every refusal below is read
        against
      - a person whose assignment DENIES crm.application.access
      - >-
        is refused the CRM route with 403, though the same assignment allows
        every CRM key
      - >-
        is refused under the code and reason a client branches on, so a console
        knows to refresh its launcher
      - >-
        a person whose grants simply never name the access key, as a custom role
        written without it leaves them
      - >-
        is refused the same route in the same words: the step asks what is
        allowed, not what was written
      - is served it once the access key is allowed beside the same read key
      - >-
        a tenant whose licence record does not name the probe route’s
        application
      - >-
        is served the probe route where the record names the application, the
        premise of the refusal
      - >-
        is refused it where the record names CRM alone, under the licence step’s
        own code
      - 'is served a CRM route in that same tenant, because crm is licence-exempt'
      - a handler asking for a key its route declares in evaluates
      - >-
        reaches the key where the access key is allowed with it, the premise of
        the refusal below
      - >-
        is refused where the assignment DENIES crm.application.access, though
        the route itself is served
      - >-
        is refused in the handler’s own words, the dispatcher having answered no
        reason code of its own
      - >-
        is refused where the grants simply never name the access key, as a
        custom role written without it leaves them
      - >-
        reaches it when no mode is in the environment, which is what shows the
        refusal is the deployed word’s
      - the modes the deployed functions are given
      - >-
        leaves the process environment as it found it, so no other suite
        inherits a mode
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies
      and transaction behavior are real dependencies.
  - provenance: test
    source: 'services-crm:modules/entities/tests/e2e/archive.e2e.spec.js'
    declared_test_titles:
      - archive API (end to end)
      - who may archive
      - 'refuses a caller who may update contacts but not archive them, 403'
      - 'refuses a caller who may only read contacts, 403'
      - 'refuses a caller who may update organizations but not archive them, 403'
      - 'refuses the unarchive to the same callers, 403'
      - left the contact live after the refusals
      - the request
      - 'refuses an archive with no version, 400, naming it'
      - 'refuses a stale version, 409'
      - answers 404 for a contact the tenant has not
      - an archived contact
      - 'answers the archive state, keeping the status it had'
      - leaves it out of the list
      - 'lists it for archived=only, and only it'
      - lists it with the live ones for archived=include
      - is still read by its id
      - 'refuses a replacement, 409 RecordArchived'
      - 'refuses a patch, 409 RecordArchived'
      - 'is unarchived to the record it was, editable again'
      - >-
        is unarchived with a reason the archive does not drop, answered as
        unarchive_reason
      - 'refuses an unarchive reason over 500 characters, 400 naming the reason'
      - an archived organization
      - answers the archive state
      - 'leaves it out of the list, and lists it for archived=only'
      - >-
        keeps the former employee findable from it under Past, by the contact’s
        id
      - >-
        keeps the former employee’s affiliation naming the organization, badged
        archived
      - 'refuses a save of the organization, 409 RecordArchived'
      - is unarchived to the organization it was
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies
      and transaction behavior are real dependencies. The harness supplies
      locally signed access contexts rather than a deployed authorizer.
  - provenance: test
    source: 'services-crm:modules/entities/tests/e2e/contact-affiliations.e2e.spec.js'
    declared_test_titles:
      - '-'
      - contact affiliations API (end to end)
      - picking an organization (set_primary_organization on a create)
      - >-
        answers the organization as the contact’s primary organization, by id
        and name
      - >-
        makes one primary affiliation of the default type other, starting today,
        open-ended, with the contact’s job title
      - reads back exactly what the create answered
      - answers no primary organization for a contact that picked none
      - changing the organization
      - 'makes the new organization the primary one, from today'
      - >-
        ends the previous primary affiliation yesterday by default ('end'), no
        longer primary and now past
      - >-
        keeps the previous affiliation open and current, no longer primary, with
        'keep'
      - >-
        starts the new affiliation on an earlier effective date and ends the
        previous one the day before it
      - >-
        leaves no primary organization when the organization is cleared, and
        ends the previous one yesterday
      - >-
        removes a previous primary that began on the day it is ended before, as
        a stay of no days
      - 'answers 400 for an effective date after today, naming the field'
      - >-
        a create carrying affiliations and roles in one call (the import’s
        shape, §13 scenario 12)
      - >-
        saves every affiliation with its roles, the primary first, then the open
        one, then the ended one
      - >-
        answers the primary affiliation’s organization as the primary
        organization
      - keeps the contact’s job title equal to the primary affiliation’s
      - gives every affiliation and role an id of its own
      - reads back exactly what the create answered
      - a replacement that sends the affiliations back as read
      - answers 200
      - >-
        keeps every affiliation and role under its id, unchanged, versions
        included
      - >-
        keeps the contact's own version, since the replacement changed nothing
        (ruling D-05)
      - reads back exactly what the replacement answered
      - >-
        answers 409 for a version the contact has moved past, and changes
        nothing
      - a JSON Patch of the affiliations
      - >-
        adds the affiliation the patch appended, keeping the others under their
        ids
      - 'ends the affiliation the patch gave an end date, now past'
      - ends the open roles of the ended affiliation the same day
      - makes the affiliation the patch marked primary the primary organization
      - 'unmarks the old primary, which stays open and current'
      - the refusals of a contact write
      - answers 400 for two affiliations newly marked primary
      - 'answers 400 for a primary affiliation with an end date, naming the field'
      - >-
        answers 400 for two overlapping affiliations to the same organization of
        the same type, naming the field
      - >-
        accepts two affiliations to the same organization of the same type in
        separate periods
      - >-
        answers 400 for a role that starts before its affiliation, naming the
        field
      - answers 400 for an organization the tenant does not have
      - saves no contact for a write it refused
      - 'GET /organizations/{tenantId}/{id}/affiliations'
      - 'answers the affiliations effective today by default, in contact order'
      - >-
        answers each affiliation with its contact, its organization and its
        roles
      - answers how many affiliations the page holds
      - answers the ended affiliations for effective=past
      - answers the affiliations not yet begun for effective=future
      - answers every affiliation for effective=all
      - answers the affiliations effective on the as_of date
      - answers only the affiliations of the affiliation_type_id given
      - answers only the affiliations holding the contact_role_id given
      - 'answers at most the limit, with a cursor for the next page'
      - >-
        answers the affiliations that follow the cursor the first page gave, and
        no cursor on the last page
      - >-
        answers 200 with no affiliations for an organization the tenant does not
        have
      - answers 400 for a cursor it did not answer
      - >-
        answers 400 for an effective value the list does not know, naming the
        parameter
      - answers 401 to an anonymous call
      - answers 403 when the signed context does not cover the path's tenant
      - >-
        answers 403 permission_missing to a caller who may read organizations
        but not contacts
      - answers a caller who may only read contacts
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies
      and transaction behavior are real dependencies. The harness supplies
      locally signed access contexts rather than a deployed authorizer.
  - provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/contact-information-sources.e2e.spec.js
    declared_test_titles:
      - contact information sources API (end to end)
      - a PUT linking the work address and number
      - 'answers the work address with its source, resolved to Acme’s values'
      - answers the work number resolved to Acme’s
      - 'takes the record read sent back as it is, resolved values and all'
      - leaves the link alone on a PATCH of another field
      - >-
        refuses a value for the linked address that is not Acme’s, naming the
        field
      - keeps an independent copy of the value when the address is saved copied
      - >-
        a PATCH that copies the linked address and removes its affiliation, and
        one that would null the addresses (CRM-ACC-0178)
      - refuses 400 naming the addresses for a patch that sets them to null
      - keeps the linked address the refused patch would have emptied
      - >-
        answers 200 for the copy-and-remove patch with the address kept as an
        independent copy
      - 'stores the independent copy: the read back has the address'
      - >-
        a POST creating a contact with a new affiliation under a
        client-generated id and an address linked through it
      - answers 200 with the affiliation under the client’s id
      - 'answers the work address linked through it, resolved to Acme’s'
      - >-
        refuses 400 a second contact naming the same affiliation id, saying
        another affiliation has it
      - a POST and a PATCH naming the organization’s records by type
      - >-
        answers 200 with the work address linked to Acme’s main address by its
        id, and no type property
      - >-
        answers the PATCH with the work number linked to Acme’s main number by
        its id, and no type property
      - 'refuses 400 a type Acme has no record of, naming the item'
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies
      and transaction behavior are real dependencies. The harness supplies
      locally signed access contexts rather than a deployed authorizer.
  - provenance: test
    source: 'services-crm:modules/entities/tests/e2e/contact-roles.e2e.spec.js'
    declared_test_titles:
      - contact roles API (end to end)
      - 'POST /contact_roles/{tenantId}'
      - answers the created role
      - answers 400 for a role without a name
      - answers 400 for an id with a space
      - >-
        creates a role with a 63-character id and a 255-character name
        (CRM-ACC-0173)
      - >-
        answers 400 naming the id for a 64-character id, never a 500 without a
        message (CRM-ACC-0173)
      - answers 401 to an anonymous call
      - answers 403 to a portal user of the tenant
      - answers 403 when the signed context does not cover the path's tenant
      - the name a role must have
      - answers 400 naming the field for a create whose name is the empty string
      - >-
        answers 400 naming the field for a create whose name is nothing but
        spaces
      - 'writes no role at all for a create it refused: the id reads back 404'
      - takes a name with spaces around it and stores it trimmed
      - a replacement and a patch that blank the name of a role that has one
      - 'refuses the replacement 400, naming the field'
      - leaves the record exactly as it was after that replacement
      - 'refuses the patch 400, naming the field'
      - leaves the record exactly as it was after that patch
      - 'GET /contact_roles/{tenantId}'
      - answers a page with a cursor to the next
      - 'answers the next page from the cursor, after the first in name order'
      - answers 400 for a cursor it did not answer
      - answers a portal user of the tenant
      - answers 401 to an anonymous call
      - 'GET /contact_roles/{tenantId}/{id}'
      - answers the role
      - answers a portal user of the tenant
      - answers 404 for a role the tenant does not have
      - answers 403 when the signed context does not cover the path's tenant
      - >-
        answers 404 for this tenant's role read in another tenant, to a context
        that covers both
      - 'PUT /contact_roles/{tenantId}/{id}'
      - answers the replaced role at the next version
      - reads back exactly what the replacement answered
      - answers 409 for a replacement at a version moved on
      - answers 400 for a body whose id is not the path id
      - 'PATCH /contact_roles/{tenantId}/{id}'
      - answers the patched role
      - clears the field the patch removed
      - answers 404 for a role the tenant does not have
      - 'a custom field value, which a contact role does not take'
      - 'refuses a create that carries custom_fields, as an unknown property'
      - refuses a replacement that carries custom_fields
      - refuses a patch that adds custom_fields
      - answers a role without a custom_fields property
      - 'PUT /contact_roles/{tenantId}/order'
      - >-
        lists the roles of a tenant that has arranged nothing alphabetically,
        each holding no place
      - >-
        answers 200 with every role of the tenant, in the order the body listed
        them, numbered 1..n
      - answers what the list answers afterwards
      - >-
        persists the new place and bumps the version of a role it moved: a later
        read answers both
      - >-
        answers 400 for a reorder that leaves a role of the tenant out, naming
        it
      - answers 400 for a reorder that names a role the tenant does not have
      - answers 403 to a caller who may only read contact roles
      - leaves the order as the successful reorder left it after every refusal
      - 'DELETE /contact_roles/{tenantId}/{id}'
      - deletes a role no contact holds
      - 'answers 409 for a role a contact holds, with the legacy words'
      - answers 404 for a role the tenant does not have
      - answers 401 to an anonymous call
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies
      and transaction behavior are real dependencies. The harness supplies
      locally signed access contexts rather than a deployed authorizer.
  - provenance: test
    source: 'services-crm:modules/entities/tests/e2e/contact-types.e2e.spec.js'
    declared_test_titles:
      - contact types API (end to end)
      - 'the lifecycle (the legacy shared lifecycle suite, step by step)'
      - >-
        is able to write a brand new record: 200 with the type as saved, version
        1, in the tenant of the path
      - 'and is then able to read that record: every field as written'
      - and can get a list containing it
      - 'then updates the whole object, bumping the version'
      - >-
        and that replacement is what a later read answers: it was persisted, not
        just returned
      - 'then, when the version is off, refuses the change (409)'
      - 'then, when the id is wrong, refuses the change (400)'
      - then patches the object
      - and leaves what the patch said nothing about as it was
      - >-
        then can be deleted, and when read nothing comes back and a 404 is
        returned, as for a second delete
      - the schema rules
      - derives the id from the name when the create names none
      - >-
        answers 400 for an id that is not a configuration id, saying what an id
        may hold
      - answers 400 for a reserved id
      - answers 400 without a name
      - answers 400 for a property the contract does not carry
      - answers 400 for an id the tenant already uses
      - answers 404 for a type the tenant does not have
      - the name a type must have
      - answers 400 naming the field for a create whose name is the empty string
      - >-
        answers 400 naming the field for a create whose name is nothing but
        spaces
      - 'writes no type at all for a create it refused: the id reads back 404'
      - takes a name with spaces around it and stores it trimmed
      - a replacement and a patch that blank the name of a type that has one
      - 'refuses the replacement 400, naming the field'
      - leaves the record exactly as it was after that replacement
      - 'refuses the patch 400, naming the field'
      - leaves the record exactly as it was after that patch
      - contacts and their type
      - refuses a contact of a type the tenant does not have (409)
      - accepts a contact of a type the tenant does have
      - refuses the delete of a type a contact has (409)
      - the order of the tenant’s types
      - >-
        lists the types of a tenant that has arranged nothing alphabetically,
        each holding no place
      - answers 200 with every type of the tenant
      - 'answers the types in the order the body listed them, numbered 1..n'
      - answers what the list answers afterwards
      - >-
        persists the new place and bumps the version of a type it moved: a later
        read answers both
      - answers 400 for a reorder that names a type the tenant does not have
      - answers 400 for a reorder that leaves a type of the tenant out
      - answers 400 for a reorder that names a type twice
      - answers 400 for a body without the ids
      - answers 403 to a caller who may only read contact types
      - leaves the order as the successful reorder left it after every refusal
      - locks and access
      - refuses changing a type locked against updates
      - refuses deleting a type locked against deletes
      - answers 401 without a context
      - answers 403 for a tenant the context does not cover
      - answers 403 to a caller who may only read contact types
      - lets a caller who may only read contact types read them
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies
      and transaction behavior are real dependencies. The harness supplies
      locally signed access contexts rather than a deployed authorizer.
  - provenance: test
    source: 'services-crm:modules/entities/tests/e2e/contacts-custom-fields.e2e.spec.js'
    declared_test_titles:
      - 'contacts API: the custom_fields map (end to end)'
      - a create with a value of every kind in the map
      - answers 200
      - 'answers each value in its JSON form, false and zero included'
      - answers a multiple selection as the array of its option ids
      - 'answers every readable field of the tenant in the map, and only those'
      - puts no custom field at the top level of the record
      - reads back exactly what the create answered
      - the published structure in two tenants with different fields
      - answers the same top-level properties in both tenants
      - >-
        answers each tenant's own fields inside the map, each by its own
        definition
      - a create with no map
      - answers every readable custom field as null
      - the conversions a field type allows
      - stores a number sent for a text field as its text
      - stores a string of digits sent for a whole-number field as the number
      - stores the string false sent for a yes/no field as false
      - >-
        stores an exactly written number sent for an amount at the field’s
        places
      - a create with an empty string for a text field
      - 'leaves the field with no value, as null clears it'
      - projection
      - >-
        answers the id, the standard field and the named custom fields, the one
        without a value as null
      - answers the whole map when the projection names custom_fields
      - projects every listed contact the same way
      - a replacement under the map semantics
      - stores the standard change of each
      - leaves every custom field as it was when the map is omitted
      - leaves every custom field as it was for an empty map
      - >-
        sets the key it sends, clears the key sent null and the selection sent
        empty, and leaves the others
      - a patch of the map
      - 'replaces, clears and appends exactly the keys the directives name'
      - >-
        merges a whole-map replacement key by key, never clearing what it leaves
        out
      - a field the API requires
      - >-
        creates a contact that gives it a value, the console-required field left
        out
      - 'refuses a create that leaves it out, naming it at its path'
      - refuses a create that sends it blank
      - >-
        saves a replace of a contact created before the field was required,
        leaving the field out
      - refuses a replace that sends it null
      - refuses a patch that removes it
      - keeps the value both clears were refused for
      - refusals
      - refuses a custom field at the top level of the record
      - 'refuses custom_fields: null on a create rather than clearing every field'
      - 'refuses custom_fields: null on a replacement'
      - refuses a patch that removes the whole map
      - refuses an object as a value
      - 'refuses a key the object does not have, at its path'
      - refuses another tenant's field as unknown in this tenant
      - refuses a number for an e-mail field instead of stringifying it
      - refuses a malformed e-mail address without echoing it
      - refuses a word for a whole number
      - 'refuses yes for a yes/no field: no truthiness'
      - refuses a text longer than the field allows
      - 'refuses an option the list does not have, without echoing it'
      - refuses an archived option
      - 'refuses a date and time without an offset, which is ambiguous'
      - refuses a reference to a record the tenant does not have
      - refuses a write to an archived field
      - >-
        refuses a replacement with an invalid custom value as a whole, its
        standard change included
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies
      and transaction behavior are real dependencies. The harness supplies
      locally signed access contexts rather than a deployed authorizer.
  - provenance: test
    source: 'services-crm:modules/entities/tests/e2e/contacts-disconnected.e2e.spec.js'
    declared_test_titles:
      - disconnected contact information API (end to end)
      - >-
        an organization that removes the address and the number a contact is
        linked to
      - 'turns the work address into a disconnected row, not a copy'
      - keeps the last known address
      - 'says where it came from, when and why the source went'
      - >-
        says it needs verification and that its source is disconnected, so no
        client draws it as synced
      - 'still has a value: resolved means "has a value", not "is synced"'
      - holds no link any more
      - turns the work phone number into a disconnected row with its own reason
      - >-
        takes the record read sent back as it is, the provenance staying the
        server’s
      - >-
        refuses a client that asks for a row to be disconnected, naming the
        field
      - verifying a disconnected address
      - 'makes it the contact’s own, with the value unchanged'
      - drops the provenance and the verification flag
      - bumps the contact’s version once
      - is refused 409 on a row that is no longer disconnected
      - relinking a disconnected address
      - links it to the organization’s address through the affiliation
      - answers the organization’s value from then on
      - carries no provenance of the disconnection any more
      - 'refuses a request naming both the record and its type, or neither'
      - removing a disconnected phone number
      - deletes the row
      - 'leaves the disconnected address, which was not asked about'
      - 'refuses a number the contact has not, 404'
      - the mutations’ preconditions
      - 'refuses a stale version, 409'
      - 'refuses a request with no version, 400, naming it'
      - 'refuses a caller who may only read contacts, 403'
      - left the address disconnected after the refusals
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies
      and transaction behavior are real dependencies. The harness supplies
      locally signed access contexts rather than a deployed authorizer.
  - provenance: test
    source: 'services-crm:modules/entities/tests/e2e/contacts-history.e2e.spec.js'
    declared_test_titles:
      - docs/record-history.md
      - contact history API (end to end)
      - >-
        answers the timeline newest first, with the live record named and not
        deleted
      - >-
        names the person who made each change, their initials and the console as
        its source
      - >-
        summarises each revision by what it changed, counting the fields and
        naming the sections
      - >-
        reads one revision in full: the section, the field, its label, its data
        type and both values
      - >-
        presents the create as the record's fields added, grouped and labelled
        as the console shows them
      - >-
        names the user who created the contact in the create’s Created By line,
        as its timeline entry names them
      - >-
        names a custom field by the tenant's own label, and its value by its
        option's
      - pages with a cursor that does not move when the record changes behind it
      - 'filters the timeline by person, operation, source and date'
      - >-
        refuses a query parameter it does not know, rather than ignoring the
        filter
      - refuses a limit above the maximum page size
      - >-
        lists everyone who has changed the record, once each, for the person
        filter
      - >-
        answers 404 for a revision of another contact, and for a contact the
        tenant does not have
      - 'never shows another tenant this contact, and refuses an anonymous call'
      - refuses a caller who may read the contact but was not given its history
      - >-
        answers a caller holding only read and history, which is the console
        reader
      - >-
        keeps a deleted contact's history readable, with the record marked
        deleted and when
      - a save that changes only the contact’s roles
      - >-
        shows the role removed and the role added, each under the name it has
        now
      - >-
        gives the added row the role it holds, labelled as the declaration
        labels a role, of the reference data type
      - >-
        counts one change for each role added or removed, and nothing else for
        the save
      - >-
        summarises the save by the roles it changed, as it summarises any other
        section
      - >-
        lists the role the contact was created holding as added, in the create’s
        own revision
      - >-
        lists the affiliation the contact was created with as added, named by
        its organization and type
      - a save that changes only an address’s coordinates
      - >-
        lists the latitude and the longitude changed under the address, at the
        precision they were saved
      - counts the two coordinates as the save’s changes
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies
      and transaction behavior are real dependencies. The harness supplies
      locally signed access contexts rather than a deployed authorizer.
  - provenance: test
    source: 'services-crm:modules/entities/tests/e2e/contacts-names.e2e.spec.js'
    declared_test_titles:
      - contact names API (end to end)
      - creating a contact
      - accepts a contact with a first name only and builds every name from it
      - accepts a contact with a last name only and builds every name from it
      - accepts a contact with a first and a last name
      - 'builds the full name from every part in order, trimmed and singly spaced'
      - >-
        answers 400 for a contact with neither a first nor a last name, naming
        the rule
      - >-
        answers 400 naming the field for a contact whose first and last names
        are blank
      - answers 400 for a contact whose first and last names are null
      - >-
        answers 400 for a full name sent instead of name parts, and never splits
        it into parts
      - answers 400 for a full name sent beside valid name parts
      - answers 400 for a name sent beside valid name parts
      - answers 400 for a sort name sent beside valid name parts
      - patching a contact
      - >-
        accepts clearing the first name while the last name remains, and
        rebuilds every name
      - leaves the name parts and fields a patch did not name as they were
      - rebuilds every name when a patch changes a name part
      - 'stores the rebuilt names, as a read answers them'
      - >-
        answers 400 for clearing the last name that remains, leaving the contact
        as it was
      - answers 400 for blanking the last name that remains
      - answers 400 for a patch that sets the full name
      - rebuilds every name when a patch clears the prefix
      - replacing a contact
      - rebuilds every name from the parts the replacement named
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies
      and transaction behavior are real dependencies. The harness supplies
      locally signed access contexts rather than a deployed authorizer.
  - provenance: test
    source: 'services-crm:modules/entities/tests/e2e/contacts.e2e.spec.js'
    declared_test_titles:
      - contacts API (end to end)
      - GET /entities/health
      - 'answers 200 publicly, with no security context'
      - answers the build identity of the service
      - 'POST /contacts/{tenantId}'
      - >-
        answers 200 with the contact in the tenant of the path, stamped with the
        caller of the context
      - computes the name from the parts the body named
      - >-
        gives the contact a UUIDv7 id when the body names none (Andrew,
        2026-10-10)
      - >-
        answers the contact number as its own property, which is no longer the
        id
      - 'keeps the id the body named, which is no UUIDv7'
      - >-
        answers the phone numbers in the read order, the preferred one the first
        in the tenant’s order
      - >-
        answers the addresses in the read order, the preferred one the first in
        the tenant’s order
      - >-
        answers 200 for a body with a blank e-mail element, dropping the element
        (DEF-CRM-0013)
      - 'answers 400 for an invalid body, listing the problems the contract names'
      - answers 400 for a body that breaks a persistence rule
      - 'answers 400 for an address latitude above 90, naming the field'
      - 'answers 400 for an address longitude below -180, naming the field'
      - answers 400 for a body that names the retired column name `version`
      - answers 400 for a body that chooses its own tenant
      - answers 400 for an element of another tenant's contact
      - answers 409 for a spouse the tenant does not have
      - answers 401 to an anonymous call
      - answers 403 when the signed context does not cover the path's tenant
      - 'GET /contacts/{tenantId}/{id}'
      - answers 200 with exactly what the create answered
      - 'answers only the fields `?fields=` named, with the id'
      - answers 404 for a contact the tenant does not have
      - answers 401 to an anonymous call
      - answers 403 when the signed context does not cover the path's tenant
      - >-
        answers the other tenant its own contact for that id, to a context that
        covers it
      - 'GET /contacts/{tenantId}/{id}/mailing_address'
      - >-
        answers the seasonal address in season when the contact marks it, with
        the workspace date
      - answers the workspace date as a calendar date
      - >-
        answers the preferred address in season when the contact has not marked
        the season
      - answers the preferred address out of season
      - answers no address for a contact with none
      - answers 404 for a contact the tenant does not have
      - answers 401 to an anonymous call
      - answers 403 when the signed context does not cover the path's tenant
      - a seasonal address saved through the contact routes
      - answers the season as month/day values
      - refuses overlapping seasons with a 400 naming both addresses
      - refuses calendar dates with a 400 naming the field
      - 'GET /contacts/{tenantId}'
      - >-
        answers 200 with the contacts of the path tenant, at most the limit, in
        number order
      - answers how many contacts the page holds
      - answers the contacts that follow the cursor the first page gave
      - answers no cursor on the last page
      - answers only the contacts of the path tenant
      - answers 400 for a limit below the range
      - answers 400 for a limit above the range
      - answers 400 for a cursor it did not answer
      - answers 401 to an anonymous call
      - answers 403 when the signed context does not cover the path's tenant
      - 'PUT /contacts/{tenantId}/{id}'
      - answers 200 with the replaced contact
      - bumps the record's version
      - 'keeps the edited element under its id, with its own version bumped'
      - removes the element the body left out and adds the one it named
      - reads back exactly what the replacement answered
      - answers 404 for a contact the tenant does not have
      - answers 409 for a version the contact has moved past
      - >-
        answers 200 for a replacement that sends back everything a read
        answered, the computed names included (M1-D5)
      - >-
        answers 400 naming the full name for a replacement that sends back
        another than the stored one (M1-D5)
      - >-
        answers 409 with the code record_changed and the latest change when a
        colleague saved first (M1-D6)
      - >-
        answers 409 with the code record_changed and the latest change for a
        patch at a version a colleague moved on (M1-D6)
      - answers 403 naming sys_locked for a create that sets a lock (E-14)
      - answers 403 naming sys_locked for a replacement that sets a lock (E-14)
      - >-
        answers 403 naming sys_locked for a replacement that sets a value that
        is no lock (CRM-ACC-0035)
      - answers 403 naming sys_locked for a patch that sets a lock (E-14)
      - >-
        answers 409 naming the system, not a colleague, when the system cleared
        the spouse link while the contact was edited (M1-D6)
      - >-
        unlocks a contact locked against deletes, at the version the lock
        answered
      - >-
        answers 409 to a replacement that only clears an element lock without
        the unlock key, and 200 with it (CRM-ACC-0035)
      - lets a patch clear one element lock by setting it to null (CRM-ACC-0035)
      - >-
        releases the locks of the contact elements with the unlock action, all
        of them when none is named (CRM-ACC-0035)
      - >-
        releases only the element ids the unlock names, and refuses an id the
        contact has no element for (CRM-ACC-0035)
      - >-
        answers 400 naming origin_service for a replacement that alters it
        (M1-D5)
      - >-
        answers 409 IllegalOperation, not 403, for a replacement of a contact
        locked against updates, the lock sent back unchanged
      - answers 400 for a replacement that names no version
      - answers 401 to an anonymous call
      - answers 403 when the signed context does not cover the path's tenant
      - leaves the other tenant's contact as it was after the refusal
      - 'GET /contacts/{tenantId}?selectable=true'
      - answers every contact when selectable is not asked for (premise)
      - >-
        answers only the contacts a picker may offer: not the inactive, not the
        deceased
      - answers every contact for selectable=false
      - answers 400 for a selectable that is neither true nor false
      - 'PATCH /contacts/{tenantId}/{id}'
      - answers 200 with the patched contact
      - sets the field the patch replaced and bumps the version
      - edits the one element the patch pointed at
      - appends the element the patch added
      - removes the element the patch removed
      - clears a field a later patch removes
      - >-
        answers 400 for a patch that sets an address latitude above 90, naming
        the field
      - answers 404 for a contact the tenant does not have
      - answers 401 to an anonymous call
      - answers 403 when the signed context does not cover the path's tenant
      - 'PATCH /contacts/{tenantId}/{id}: the version precondition (ruling D-05)'
      - 'applies a patch that tests the version read, and bumps it'
      - answers 409 to a second patch at the version the first one moved on from
      - answers 400 naming sys_version to a patch that tests no version
      - keeps the first writer’s value
      - 'DELETE /contacts/{tenantId}/{id}'
      - answers 200 with the id it deleted
      - 'leaves nothing to read: a later GET answers 404'
      - answers 404 to a second delete of the same id
      - answers 404 for a contact the tenant never had
      - answers 401 to an anonymous call
      - answers 403 when the signed context does not cover the path's tenant
      - leaves the other tenant's contact in place after the refusal
      - the contact's type
      - answers the create with the type the body named
      - keeps the type through a patch that says nothing about it
      - clears the type when a patch sets it to null
      - 'answers 409 for a type the tenant does not have, and saves no contact'
      - the permissions the routes declare
      - >-
        answers 403 to a slice that grants reading contacts and nothing else,
        and 200 to the reads
      - >-
        answers 401 ctx_key_missing when the slice was resolved for keys this
        code does not require
      - 'POST /contacts/{tenantId} with a photo'
      - answers 200 to a body whose photo is a staging upload
      - >-
        answers the permanent reference the record stores, never the upload the
        client sent
      - >-
        answers 400 for a photo whose upload is not there, naming the field that
        failed
      - 'GET /contacts/{tenantId}/descendants/{organization_id}'
      - 'a parent organization, without fieldsToReturn'
      - answers 200
      - 'answers the organization’s own contact first, then its child’s'
      - answers whole contact records
      - answers each contact with its collections
      - fieldsToReturn
      - 'projects each contact to the named fields, always with its id'
      - a child organization
      - answers only its own subtree
      - an organization the tenant does not have
      - answers 200 with an empty array
      - refusals
      - answers 401 to an anonymous call
      - answers 403 in a tenant the context does not cover
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies
      and transaction behavior are real dependencies. File storage uses an
      in-memory double; storage failures are injected by that double. The
      harness supplies locally signed access contexts rather than a deployed
      authorizer. The suite also supplies the collaborator replacements declared
      in its source; those replacements delimit its external-service claims.
  - provenance: test
    source: 'services-crm:modules/entities/tests/e2e/display-links.e2e.spec.js'
    declared_test_titles:
      - display links on the contact and organization reads (end to end)
      - 'GET /contacts/{tenantId}/{id}'
      - >-
        answers the public image route for the photo, beside the stored
        reference
      - answers the public image route for an image custom field
      - >-
        answers a presigned GET that lasts 15 minutes and downloads as an
        attachment for a document custom field
      - >-
        links no field that is not a file: a text custom field holding an
        address has no entry
      - leaves the link out of a read that projects the file away
      - adds no display_links to a contact that holds no file
      - accepts a read sent back as a replacement and does not store the links
      - 'GET /contacts/{tenantId}'
      - 'links the photo of the contact in the page, and only that contact'
      - 'GET /organizations/{tenantId}/{id}'
      - answers the logo link and the document link beside the stored references
      - 'GET /organizations/{tenantId}'
      - links the logo of the organization in the page
      - a write
      - >-
        answers the stored references without links: only the read routes make
        them
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies
      and transaction behavior are real dependencies. File storage uses an
      in-memory double; storage failures are injected by that double. The
      harness supplies locally signed access contexts rather than a deployed
      authorizer. The suite also supplies the collaborator replacements declared
      in its source; those replacements delimit its external-service claims.
  - provenance: test
    source: 'services-crm:modules/entities/tests/e2e/email-previews.e2e.spec.js'
    declared_test_titles:
      - email previews API (end to end)
      - the allowed call
      - >-
        answers 200 with the content rendered from the records, and no sample
        data for an organization that really breaks a rule
      - derives the text body from the HTML when the template has none
      - 'answers the text body the template states, when it states one'
      - a template the author must fix
      - >-
        answers 200 with the problem, because a template’s problems are an
        answer and not a refusal
      - the refusals of the contract
      - answers 400 for a property the schema does not know
      - answers 400 for a body with no template at all
      - >-
        answers 400 for a purpose this service does not preview, naming the ones
        it does
      - answers 400 for a contact that is not linked to the organization
      - answers 404 for an organization the tenant does not have
      - answers 404 for a contact the tenant does not have
      - the refusals of the authorization
      - >-
        answers 401 without a context, and 403 for a tenant the context does not
        cover
      - >-
        answers 403 to a caller who may only read organizations: the route is
        the template administrator’s
      - >-
        answers 403 to a caller who may administer types but may not read the
        records the answer contains
      - admits a caller holding the administration key and both read keys
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies
      and transaction behavior are real dependencies. Platform communication
      operations use the local test adapter, so this suite does not prove
      deployed template validation or delivery. The harness supplies locally
      signed access contexts rather than a deployed authorizer.
  - provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/organization-compliance.e2e.spec.js
    declared_test_titles:
      - contact role compliance API (end to end)
      - the organization routes
      - >-
        answer the computed status and violations of an organization created
        with a compliance type
      - keep them against a PUT that sends others back
      - 'POST /organizations/{tenantId}/reprocess_compliance'
      - answers 200 with the legacy message counting the tenant's organizations
      - >-
        answers 401 without a context, and 403 for a tenant the context does not
        cover
      - answers 403 to a caller who may only read organizations
      - 'POST /organizations/{tenantId}/preview_contact_role_compliance_email'
      - >-
        answers 200 with Platform's answer to the one test send: sent, with its
        message id
      - 'answers 400 for a typed-in template without a subject, naming the field'
      - 'answers 400 for a template both named and typed in, or neither'
      - answers 404 for a contact the tenant does not have
      - >-
        answers 401 without a context, and 403 for a tenant the context does not
        cover
      - answers 403 to a caller who may only read organizations
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies
      and transaction behavior are real dependencies. The harness supplies
      locally signed access contexts rather than a deployed authorizer.
  - provenance: test
    source: 'services-crm:modules/entities/tests/e2e/organization-types.e2e.spec.js'
    declared_test_titles:
      - organization types API (end to end)
      - 'the lifecycle (the legacy shared lifecycle suite, step by step)'
      - >-
        is able to write a brand new record: 200 with the type as saved, version
        1, in the tenant of the path
      - 'runs the legacy cleaning on save: rules with neither bound dropped'
      - 'and is then able to read that record: every field as written'
      - and can get a list containing it
      - 'then updates the whole object, bumping the version'
      - >-
        and reads the replacement back, the whole record as the replace answered
        it
      - >-
        and the replacement replaced the type's owned compliance rules, rather
        than adding to them
      - 'then, when the version is off, refuses the change (409)'
      - 'then, when the id is wrong, refuses the change (400)'
      - then patches the object
      - >-
        then can be deleted, and when read nothing comes back and a 404 is
        returned, as for a second delete
      - the legacy schema rules
      - derives the id from the name when the create names none
      - >-
        answers 400 for an id that is not a configuration id, naming the rule,
        and writes no type
      - answers 400 without a name and for an unknown property
      - the name a type must have
      - answers 400 naming the field for a create whose name is the empty string
      - >-
        answers 400 naming the field for a create whose name is nothing but
        spaces
      - 'writes no type at all for a create it refused: the id reads back 404'
      - takes a name with spaces around it and stores it trimmed
      - a replacement and a patch that blank the name of a type that has one
      - 'refuses the replacement 400, naming the field'
      - leaves the record exactly as it was after that replacement
      - 'refuses the patch 400, naming the field'
      - leaves the record exactly as it was after that patch
      - >-
        answers 400 naming the property for enableKeyRoles and for key_roles,
        which the schema no longer knows (removed 2026-10-11)
      - 'answers no key-role property on a create, a read or a list'
      - >-
        answers 400 for each of Rhythm 1.0's five form ids, which the schema no
        longer knows (removed 2026-10-10)
      - >-
        answers 400 when a switch is on and its list is missing, naming the
        field
      - >-
        accepts a type that notifies with no e-mail template, and refuses one
        that sends a template (the e-mail is Platform's)
      - >-
        answers 409 for a rule of a role the tenant does not have, and saves
        nothing
      - the compliance notification template the type composes
      - >-
        answers the property on a create, a read and a list, and null for a type
        that owns no template
      - leaves the template alone when a patch changes only the name
      - >-
        merges the fields a patch of the property names, keeping the rest of the
        wording
      - >-
        clears the template when a replace sends the property as null, and the
        type then owns none
      - >-
        answers 400 naming the property for a template Platform refuses, and
        writes no type
      - organizations and their type
      - >-
        refuses an organization of a type the tenant does not have (409), and
        the delete of a type an organization has (409)
      - the order of the tenant’s types
      - >-
        lists the types of a tenant that has arranged nothing alphabetically,
        each holding no place
      - >-
        answers 200 with every type of the tenant, in the order the body listed
        them, numbered 1..n
      - answers what the list answers afterwards
      - >-
        persists the new place and bumps the version of a type it moved: a later
        read answers both
      - >-
        answers 400 for a reorder that leaves a type of the tenant out, naming
        it
      - answers 400 for a reorder that names a type the tenant does not have
      - answers 403 to a caller who may only read organization types
      - leaves the order as the successful reorder left it after every refusal
      - locks and access
      - >-
        refuses changing a type locked against updates, and deleting one locked
        against deletes
      - >-
        answers 401 without a context, and 403 for a tenant the context does not
        cover
      - >-
        answers 404 in another tenant for a type this tenant has, to a caller
        that tenant's context does cover
      - >-
        answers 403 to a caller who may only read organization types, and lets
        that caller read
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies
      and transaction behavior are real dependencies. Platform communication
      operations use the local test adapter, so this suite does not prove
      deployed template validation or delivery. The harness supplies locally
      signed access contexts rather than a deployed authorizer.
  - provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/organizations-custom-fields.e2e.spec.js
    declared_test_titles:
      - 'organizations API: the custom_fields map (end to end)'
      - a create with a value of every kind in the map
      - 'answers each value in its JSON form, false and zero included'
      - reads back exactly what the create answered
      - does not answer a contact's custom field on an organization
      - a replacement and a patch under the map semantics
      - >-
        stores the standard and the custom change of the replacement together,
        leaving the other keys
      - clears only the key the patch removes
      - a conversion
      - stores the string of digits as the whole number
      - a field the API requires
      - creates an organization that selects one
      - refuses a create that leaves it out
      - refuses a create that sends an empty selection
      - >-
        saves a patch of an organization created before the field was required,
        leaving the field out
      - refuses a patch that empties the selection
      - refusals
      - 'refuses custom_fields: null'
      - refuses a contact's custom field as unknown on an organization
      - refuses yes for a yes/no field
      - refuses a number that cannot be written exactly for a percentage
      - refuses a reference to a contact the tenant does not have
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies
      and transaction behavior are real dependencies. The harness supplies
      locally signed access contexts rather than a deployed authorizer.
  - provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/organizations-domain-name.e2e.spec.js
    declared_test_titles:
      - organization website and domain name through the API (end to end)
      - 'POST /organizations/{tenantId} with a website and no domain name'
      - answers 200 with the domain name derived from the website
      - >-
        PATCH /organizations/{tenantId}/{id} removing the domain name of an
        organization that has a website
      - >-
        answers 200 with the domain name derived again from the website the
        organization kept
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies
      and transaction behavior are real dependencies. The harness supplies
      locally signed access contexts rather than a deployed authorizer.
  - provenance: test
    source: 'services-crm:modules/entities/tests/e2e/organizations-history.e2e.spec.js'
    declared_test_titles:
      - docs/record-history.md
      - organization history API (end to end)
      - the timeline
      - 'names the organization, live and not deleted'
      - 'has one revision per committed save, newest first'
      - names the person who made each change and the console as its source
      - lists the people who changed it
      - one revision
      - >-
        shows the unarchive reason as a field of the organization, with its
        label
      - shows the archive and its reason in the revision before
      - 'shows an address as a child of the Addresses section, named by its type'
      - 'shows the rename with its label, data type and both values'
      - who may read it
      - 'refuses a caller without the history key, 403'
      - answers a reader with the history key
      - >-
        answers a caller with the history key but no read of organizations 404,
        as the organization itself would
      - 'does not show another tenant the organization, 403 or 404'
      - answers 404 for an organization that never existed
      - a deleted organization
      - 'keeps its history, marked deleted'
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies
      and transaction behavior are real dependencies. The harness supplies
      locally signed access contexts rather than a deployed authorizer.
  - provenance: test
    source: 'services-crm:modules/entities/tests/e2e/organizations.e2e.spec.js'
    declared_test_titles:
      - organizations API (end to end)
      - 'POST /organizations/{tenantId}'
      - answers 200 with the organization in the tenant of the path
      - 'stores the fields the body named, the parent among them'
      - >-
        gives the organization a UUIDv7 id when the body names none (Andrew,
        2026-10-10)
      - >-
        answers the organization number as its own property, which is no longer
        the id
      - 'keeps the id the body named, which is no UUIDv7'
      - stores the addresses in the tenant’s type order
      - stores the phone numbers in the tenant’s type order
      - answers 400 for a body with no name
      - >-
        answers 400 for an id that breaks the id grammar, naming the characters
        an id may hold
      - answers 400 for a body with two addresses of one type
      - answers 400 for a phone number type the tenant does not have
      - answers 409 for a parent the tenant does not have
      - answers 401 to an anonymous call
      - answers 403 when the signed context does not cover the path's tenant
      - 'GET /organizations/{tenantId}/{id}'
      - answers 200 with exactly what the create answered
      - 'answers only the fields `?fields=` named, with the id'
      - answers 404 for an organization the tenant does not have
      - answers 401 to an anonymous call
      - answers 403 when the signed context does not cover the path's tenant
      - >-
        answers the other tenant its own organization for that id, to a context
        that covers it
      - 'GET /organizations/{tenantId}'
      - answers 200 with at most the limit
      - answers a cursor when organizations follow the page
      - answers the rest of the tenant organizations after that cursor
      - answers only the organizations of the path tenant
      - answers 401 to an anonymous call
      - answers 403 when the signed context does not cover the path's tenant
      - 'PUT /organizations/{tenantId}/{id}'
      - answers 200 with the replaced organization
      - bumps the record's version
      - 'keeps the edited element under its id, with its own version bumped'
      - removes the element the body left out and adds the one it named
      - reads back exactly what the replacement answered
      - answers 404 for an organization the tenant does not have
      - answers 409 for a version the organization has moved past
      - answers 401 to an anonymous call
      - answers 403 when the signed context does not cover the path's tenant
      - 'PATCH /organizations/{tenantId}/{id}'
      - answers 200 with the patched organization
      - edits the one element the patch pointed at
      - appends the element the patch added
      - clears the field the patch removed
      - sets the list the patch added
      - answers 404 for an organization the tenant does not have
      - answers 401 to an anonymous call
      - answers 403 when the signed context does not cover the path's tenant
      - a lock set by a replacement (CRM-ACC-0035)
      - answers 403 naming sys_locked for a value that is no lock
      - element locks of an organization (CRM-ACC-0129)
      - >-
        answers 409 to a replacement that only clears an element lock without
        the unlock key, and 200 with it
      - lets a patch clear one element lock by setting it to null
      - releases the locks of the organization elements with the unlock action
      - 'DELETE /organizations/{tenantId}/{id}'
      - answers 200 with the id it deleted
      - 'leaves nothing to read: a later GET answers 404'
      - answers 404 to a second delete of the same id
      - >-
        answers 409 for an organization that other organizations name as their
        parent
      - answers 401 to an anonymous call
      - answers 403 when the signed context does not cover the path's tenant
      - an organization a contact is affiliated with
      - >-
        accepts the contact that picks it as its organization, which answers it
        as its primary organization
      - answers 409 to a delete while the contact is affiliated with it
      - accepts the delete once the contact has gone
      - the permissions the routes declare
      - >-
        answers 403 to a slice that grants reading organizations and nothing
        else, and 200 to the reads
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies
      and transaction behavior are real dependencies. The harness supplies
      locally signed access contexts rather than a deployed authorizer.
  - provenance: test
    source: 'services-crm:modules/entities/tests/e2e/page-layouts.e2e.spec.js'
    declared_test_titles:
      - page layouts and forms API (end to end)
      - 'a record type’s five assignments, through the whole of its API'
      - 'writes the five ids the create stated, and answers them'
      - >-
        answers the same five on a later read: they were persisted, not just
        returned
      - carries them in the list too
      - >-
        KEEPS all five through a PUT that does not mention them: an older client
        cannot wipe a tenant’s assignments
      - and a read after that PUT says the same
      - >-
        CLEARS the one a PATCH removes, and keeps the rest: the opposite of the
        same silence in a PUT
      - clears one a PATCH replaces with null
      - an assignment the tenant may not make
      - 'refuses an id no definition has (400), naming the field'
      - >-
        refuses a definition of the organizations purpose on a contact type
        (400)
      - >-
        refuses a portal form in the console edit form slot (400), naming that
        slot
      - writes no type for a refused create
      - the tenant’s defaults
      - >-
        answers both sections empty at version 0 for a tenant that has saved
        none
      - 'saves a default and answers it, at version 1'
      - leaves a section a later save does not name
      - refuses a save from a stale version (409)
      - refuses an unknown property rather than ignoring it (400)
      - a caller holding only the contact layout keys
      - is answered the section it may read
      - >-
        is not answered the section it may not read: it is ABSENT, not null, so
        "forbidden" and "unset" never look alike
      - >-
        is not answered the default affiliation type, which the affiliation
        types keys govern
      - may save its own section
      - is refused (403) when it saves the other section
      - >-
        is refused (403) for a body naming both sections, though it may change
        one of them
      - >-
        writes NEITHER section for that refused save: the decision is made
        before anything is written
      - what is actually in force
      - answers the type’s own definition for the kind the type assigns
      - falls back to the tenant’s default for a kind the type leaves empty
      - says a kind nothing configures is not configured
      - answers the tenant’s defaults when no type is named
      - answers only the console kinds for ?context=console
      - >-
        answers the create and the edit form separately, each from the default
        that configures it
      - >-
        answers the organizations entity from its own section, under its own
        purpose
      - >-
        refuses a type_id the tenant does not have (404) rather than answering
        the defaults
      - refuses a request that names no entity (400)
      - a caller holding only crm.contacts.read
      - >-
        is shown the contact definitions: being told which layout is in force is
        part of reading the record
      - 'is refused the organization ones (403): the key is the entity’s own'
      - >-
        is refused the tenant’s defaults (403): reading a record does not make
        one an administrator of layouts
      - a caller whose context does not cover the tenant in the path
      - is refused the tenant’s defaults (403)
      - is refused the resolution (403)
      - an anonymous caller
      - is refused the tenant’s defaults (401)
      - is refused the resolution (401)
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies
      and transaction behavior are real dependencies. The harness supplies
      locally signed access contexts rather than a deployed authorizer.
  - provenance: test
    source: 'services-crm:modules/entities/tests/e2e/phone-number-types.e2e.spec.js'
    declared_test_titles:
      - phone number types API (end to end)
      - 'the lifecycle (the shared lifecycle suite, step by step)'
      - >-
        is able to write a brand new record: 200 with the type as saved, version
        1, in the tenant of the path
      - >-
        puts a new type at the end of the tenant’s order when the body names no
        place
      - 'and is then able to read that record: every field as written'
      - and can get a list containing it
      - answers the list in the tenant’s display order
      - 'then updates the whole object, bumping the version'
      - >-
        and that replacement is what a later read answers: it was persisted, not
        just returned
      - 'then, when the version is off, refuses the change (409)'
      - 'then, when the id is wrong, refuses the change (400)'
      - then patches the object
      - and leaves what the patch said nothing about as it was
      - >-
        then can be deleted, and when read nothing comes back and a 404 is
        returned, as for a second delete
      - the schema rules
      - derives the id from the name when the create names none
      - answers 400 for an id that is not a configuration id
      - answers 400 for an id the API keeps for itself
      - applies a type to contacts when the create says nothing
      - answers 400 without a name
      - answers 400 for an applicability the contract does not carry
      - answers 400 for a property the contract does not carry
      - answers 400 for an id the tenant already uses
      - answers 404 for a type the tenant does not have
      - the list of the types a record may use
      - >-
        answers only the types usable on organizations when the query asks for
        them
      - answers a type of both in the lists of each kind
      - answers 400 for an applicability the list does not take
      - the order of the tenant’s types
      - answers 200 with every type of the tenant
      - 'answers the types in the order the body listed them, numbered 1..n'
      - answers what the list answers afterwards
      - >-
        persists the new place and bumps the version of a type it moved: a later
        read answers both
      - answers 400 for a reorder that names a type the tenant does not have
      - answers 400 for a reorder that leaves a type of the tenant out
      - answers 400 for a reorder that names a type twice
      - answers 400 for a body without the ids
      - leaves the order as the successful reorder left it after every refusal
      - a type a record uses
      - accepts a contact phone number of a type that applies to both
      - accepts an organization phone number of the same type
      - refuses the delete of a type a number has (409)
      - says which records keep the type when it refuses the delete
      - >-
        refuses a contact phone number of a type marked for organizations (400),
        naming the type field (CRM-M1 decision M1-D8)
      - locks and access
      - refuses changing a type locked against updates
      - refuses deleting a type locked against deletes
      - answers 401 without a context
      - answers 403 for a tenant the context does not cover
      - answers 403 to a write in a tenant the context does not cover
      - answers 403 to a caller who may only read phone number types
      - answers 403 to a read-only caller’s reorder
      - lets a caller who may only read phone number types read them
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies
      and transaction behavior are real dependencies. The harness supplies
      locally signed access contexts rather than a deployed authorizer.
  - provenance: test
    source: 'services-crm:modules/entities/tests/e2e/platform-scope.e2e.spec.js'
    declared_test_titles:
      - a platform-scope staff slice on the CRM API (end to end)
      - 'walks every protected route the macroservice registers, of every module'
      - /
      - >-
        walks console and portal routes alike, and no system route, which is why
        rule S1 leaves the API nothing
      - >-
        names a route to read for every module that declares one, so a new
        module is not walked past
      - >-
        lets an ordinary customer-scoped grant over the tenant past
        authorization on every one of them
      - >-
        serves a customer administrator a representative route of every module,
        200
      - >-
        refuses every one of them to a staff slice reaching every tenant, 403 at
        the tenant boundary
      - >-
        refuses every one of them to the same slice over the tenant named, 403
        for want of the permission
      - >-
        refuses the same staff slice every service route, 403 context_mismatch,
        because no signed context reaches one
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies
      and transaction behavior are real dependencies. The harness supplies
      locally signed access contexts rather than a deployed authorizer.
product_intent: proposed
implementation_evidence: observed
narrative:
  status: enriched
retries: none
proves_controls:
  - TC-AZ-002
lifecycles:
  create: exercised
  change: exercised
  archive: exercised
  delete: exercised
  merge: not-applicable
  reconcile: partial
lifecycle_gaps:
  - FND-CRM-ENT-095
command: npx mocha --timeout 60000 'modules/entities/tests/e2e/**/*.spec.js'
---

# Entities local API lifecycle

## Verification Boundary

Public local dispatch crosses signature, authorization, schema and PostgreSQL boundaries.

## What This Suite Proves

The enforcing dispatcher checks signed contexts and route declarations over PostgreSQL. Contact and organization writes are read again through public operations; catalogue lifecycle helpers perform creation, replacement, patch and deletion with later reads. Archive requests establish state, edit refusals and restoration for both record kinds. Unauthorized workspace and read-only contexts are denied. The refusal cases establish declared operation authorization; no deployed identity result is claimed.

## What It Does Not Prove

The locally generated signing key does not exercise the deployed Platform authorizer, Cognito or API Gateway. In-memory storage and local template adapters do not establish AWS storage or final notice delivery. An accepted reprocess request establishes dispatch, not terminal settlement of every organization.

## Level and Rationale

Request-level refusals depend on the declared operation requirements and signed context, which direct service tests bypass. This tier is the lowest local tier that crosses those guards and still establishes stored effects. Its local credentials intentionally stop short of deployed identity and infrastructure qualification.

## Lifecycle Coverage

Create and change are exercised for contacts, organizations, relationship subrecords and classification catalogues through public dispatch and later reads. Archive/restoration are exercised for contacts and organizations; classification inactive status remains a change. Delete is exercised for public record and catalogue operations with absent-read assertions. Merge belongs to Data Quality. Reconcile is partial: date reconciliation establishes its watermark by a subsequent request, while compliance reprocess tests assert request acceptance and authority rather than terminal stored progress.

## Real Dependencies and Test Doubles

The dispatcher, route declarations, signed-context verification, service functions and PostgreSQL are real. A locally generated key replaces deployed authentication, and selected photo cases use in-memory asset storage. The local communication adapter replaces deployed Platform publication or delivery. These substitutions remain limited to the source-declared seams.

## Must Not Be Doubled

Keep the real dispatcher, permission evaluation, route schemas, service operations and PostgreSQL. A permissive replacement context or route would conceal denied access; a database double would erase constraints and rollback behavior. The test signing key changes the issuer environment while preserving signature verification and grant evaluation.

## Fixtures and Isolation

The shared stages generate a test signing key, prepare valid route contexts and create scope-owned records. Teardown removes dependents and restores injected storage or communication adapters. A failed stage propagates its cause rather than letting missing data masquerade as a valid 404 or permission refusal.

## Determinism and Retries

Contexts use explicitly chosen grants and workspace coverage. The version-precondition tests preserve the first writer’s stored value after the stale request. Date reconciliation repeats a request to inspect the settled watermark, not to hide a failed first attempt. Mocha runs serially with no retries.

## How It Runs

Use the exact command in the frontmatter from the owning repository. Prepare the test database before running it. This is the Entities selection of the local API gate, wider repository scripts also select other modules. The register remains not-run because this authoring pass produces no execution report.

## Coverage Gaps

The lifecycle findings linked above identify every applicable partial or absent lifecycle in this register. The locally generated signing key does not exercise the deployed Platform authorizer, Cognito or API Gateway. In-memory storage and local template adapters do not establish AWS storage or final notice delivery. An accepted reprocess request establishes dispatch, not terminal settlement of every organization. Existing unbound files supply engineering observations; they do not silently implement additional capability or screen scenarios.
