---
id: TST-CRM-ENT-787
title: Contact aggregate persistence
kind: integration
product_area: crm
module: entities
revision: 1
status: draft
source_sha:
  services-crm: a8d1364f1dd44b723bb5f9acba12f983e9ec8d69
repository: services-crm
files:
  - modules/entities/tests/integration/db/contact-addresses.table.integration.spec.js
  - modules/entities/tests/integration/db/contact-communication-preferences.table.integration.spec.js
  - modules/entities/tests/integration/db/contact-custom-field-values.table.integration.spec.js
  - modules/entities/tests/integration/db/contact-email-addresses.table.integration.spec.js
  - modules/entities/tests/integration/db/contact-phone-numbers.table.integration.spec.js
  - modules/entities/tests/integration/db/contact-roles.table.integration.spec.js
  - modules/entities/tests/integration/db/contacts.table.integration.spec.js
  - modules/entities/tests/integration/services/contacts/contact-create.integration.spec.js
  - modules/entities/tests/integration/services/persistence/contact-roles.integration.spec.js
  - modules/entities/tests/integration/services/persistence/contact-roles.queries.integration.spec.js
  - modules/entities/tests/integration/services/persistence/contacts-record.queries.integration.spec.js
  - modules/entities/tests/integration/services/persistence/contacts.archive.integration.spec.js
  - modules/entities/tests/integration/services/persistence/contacts.conflicts.integration.spec.js
  - modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js
  - modules/entities/tests/integration/services/persistence/contacts.integration.spec.js
  - modules/entities/tests/integration/services/persistence/contacts.queries.integration.spec.js
  - modules/entities/tests/integration/services/persistence/contacts.seasonal-addresses.integration.spec.js
  - modules/entities/tests/integration/services/persistence/contacts.spouse.integration.spec.js
  - modules/entities/tests/integration/services/persistence/endpoint-deletions.integration.spec.js
implements:
  - VT-IMC-CRM-ENT-106-01
  - VT-IMC-CRM-ENT-106-02
  - VT-IMC-CRM-ENT-109-01
  - VT-IMC-CRM-ENT-109-02
  - ATS-CRM-ENT-007-01
  - ATS-CRM-ENT-007-02
  - ATS-CRM-ENT-007-03
  - ATS-CRM-ENT-005-02
  - ATS-CRM-ENT-014-02
  - ATS-CRM-ENT-001-10
  - ATS-CRM-ENT-001-08
  - ATS-CRM-ENT-017-03
  - ATS-CRM-ENT-013-02
  - ATS-CRM-ENT-013-03
  - ATS-CRM-ENT-013-05
  - ATS-CRM-ENT-013-09
  - ATS-CRM-ENT-001-05
  - ATS-CRM-ENT-004-06
  - ATS-CRM-ENT-004-08
  - ATS-CRM-ENT-005-06
  - ATS-CRM-ENT-005-07
  - ATS-CRM-ENT-001-04
  - ATS-CRM-ENT-001-06
  - ATS-CRM-ENT-001-07
  - ATS-CRM-ENT-003-04
  - ATS-CRM-ENT-004-04
  - ATS-CRM-ENT-004-05
  - ATS-CRM-ENT-004-07
  - ATS-CRM-ENT-005-01
  - ATS-CRM-ENT-005-03
  - ATS-CRM-ENT-005-04
  - ATS-CRM-ENT-005-05
  - ATS-CRM-ENT-009-07
  - ATS-CRM-ENT-009-09
  - ATS-CRM-ENT-014-03
  - ATS-CRM-ENT-014-04
  - ATS-CRM-ENT-003-02
  - ATS-CRM-ENT-003-03
  - ATS-CRM-ENT-003-05
  - ATS-CRM-ENT-001-11
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: >-
  Specs create their own contacts, counterpart records and collection rows with the shared test identifiers.
  Their stages provide a valid premise before each refusal. Cleanup removes owned rows and restores injected
  pipeline dependencies; run this cohesive group serially because process-wide dependency seams are changed by
  selected specs.
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-007-01
    source: >-
      services-crm:modules/entities/tests/integration/db/contact-communication-preferences.table.integration.spec.js#refuses
      a second preference for one communication type by contact_communication_preferences_type_uq
  - scenario: ATS-CRM-ENT-007-02
    source: >-
      services-crm:modules/entities/tests/integration/db/contact-communication-preferences.table.integration.spec.js#refuses
      a subscription address with an uppercase letter by the domain’s email_address_canonical_check
  - scenario: ATS-CRM-ENT-007-02
    source: >-
      services-crm:modules/entities/tests/integration/db/contact-communication-preferences.table.integration.spec.js#refuses
      a subscription value that is not shaped like an address by the domain’s email_address_format_check
  - scenario: ATS-CRM-ENT-007-03
    source: >-
      services-crm:modules/entities/tests/integration/db/contact-communication-preferences.table.integration.spec.js#refuses
      a subscription status outside the closed list by contact_communication_preferences_status_check
  - scenario: ATS-CRM-ENT-007-03
    source: >-
      services-crm:modules/entities/tests/integration/db/contact-communication-preferences.table.integration.spec.js#refuses
      a row that does not say whether the preference is enabled, naming the column
  - scenario: ATS-CRM-ENT-005-02
    source: >-
      services-crm:modules/entities/tests/integration/db/contact-email-addresses.table.integration.spec.js#refuses
      a second contact the same primary address on insert
  - scenario: ATS-CRM-ENT-005-02
    source: >-
      services-crm:modules/entities/tests/integration/db/contact-email-addresses.table.integration.spec.js#accepts
      another contact’s primary address as a secondary address (slot 2)
  - scenario: ATS-CRM-ENT-005-02
    source: >-
      services-crm:modules/entities/tests/integration/db/contact-email-addresses.table.integration.spec.js#releases
      the primary address of a deceased contact: another contact may hold it
  - scenario: ATS-CRM-ENT-005-02
    source: >-
      services-crm:modules/entities/tests/integration/db/contact-email-addresses.table.integration.spec.js#refuses
      to make an inactive contact active while another contact holds its primary address
  - scenario: ATS-CRM-ENT-005-02
    source: >-
      services-crm:modules/entities/tests/integration/db/contact-email-addresses.table.integration.spec.js#keeps
      the primary address of a contact marked duplicate reserved
  - scenario: ATS-CRM-ENT-014-02
    source: >-
      services-crm:modules/entities/tests/integration/db/contact-email-addresses.table.integration.spec.js#frees
      its primary address at once: its addresses went with it
  - scenario: ATS-CRM-ENT-001-10
    source: >-
      services-crm:modules/entities/tests/integration/db/contacts.table.integration.spec.js#refuses a status
      outside the closed list by contacts_status_check
  - scenario: ATS-CRM-ENT-001-08
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/contact-create.integration.spec.js#answers
      the retry with the contact the first attempt made
  - scenario: ATS-CRM-ENT-001-08
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/contact-create.integration.spec.js#refuses
      it as a conflict (409), naming the id
  - scenario: ATS-CRM-ENT-017-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-roles.integration.spec.js#refuses
      it with the legacy words and the number of holders
  - scenario: ATS-CRM-ENT-017-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-roles.integration.spec.js#refuses
      it with the legacy words, naming the organization types whose rules depend on it
  - scenario: ATS-CRM-ENT-013-02
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.archive.integration.spec.js#leaves
      the contact live after a stale version
  - scenario: ATS-CRM-ENT-013-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.archive.integration.spec.js#sets
      the instant, the user and the reason
  - scenario: ATS-CRM-ENT-013-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.archive.integration.spec.js#bumps
      the version once and stamps the caller
  - scenario: ATS-CRM-ENT-013-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.archive.integration.spec.js#keeps
      the status it had: archive is not a status
  - scenario: ATS-CRM-ENT-013-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.archive.integration.spec.js#keeps
      every collection and the affiliation, unchanged
  - scenario: ATS-CRM-ENT-013-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.archive.integration.spec.js#writes
      one revision of its own, naming the archive columns and nothing else of the record
  - scenario: ATS-CRM-ENT-013-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.archive.integration.spec.js#archived
      again answers as it is, with no new version
  - scenario: ATS-CRM-ENT-013-05
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.archive.integration.spec.js#is
      unchanged after a refused save
  - scenario: ATS-CRM-ENT-013-05
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.archive.integration.spec.js#is
      saved by a merge, which names the one save an archived record accepts
  - scenario: ATS-CRM-ENT-013-09
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.archive.integration.spec.js#is
      kept through a plain save, which never writes it
  - scenario: ATS-CRM-ENT-001-05
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#refuses
      a first name of 201 characters, naming first_name
  - scenario: ATS-CRM-ENT-001-05
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#refuses
      notes of 10,001 characters, naming notes
  - scenario: ATS-CRM-ENT-001-05
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#refuses
      an e-mail address of 255 characters, naming the element’s email_address
  - scenario: ATS-CRM-ENT-001-05
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#refuses
      a limit on update as on create
  - scenario: ATS-CRM-ENT-004-06
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#writes
      nothing when it refuses a changed computed name
  - scenario: ATS-CRM-ENT-004-08
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#names
      every overlong name part of one request at once
  - scenario: ATS-CRM-ENT-005-06
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#accepts
      a country by name and stores its code
  - scenario: ATS-CRM-ENT-005-06
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#refuses
      an unknown country, naming the address’s country
  - scenario: ATS-CRM-ENT-005-07
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js#leaves
      a contact saveable after the type of an address it holds became organizations-only
  - scenario: ATS-CRM-ENT-001-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#moves
      an honorific salutation to the prefix, start-cased and dotted (RB-2367)
  - scenario: ATS-CRM-ENT-001-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#drops
      a nickname equal to the first name, ignoring case (RS-2647)
  - scenario: ATS-CRM-ENT-001-06
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      an id the tenant already uses, by contacts_pk
  - scenario: ATS-CRM-ENT-001-07
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#takes
      the tenant's next number from platform.number_seeds
  - scenario: ATS-CRM-ENT-001-07
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#keeps
      the number consumed when the transaction rolls back: a gap, never given back
  - scenario: ATS-CRM-ENT-001-07
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#saves
      the contact under the highest number plus one, 4001
  - scenario: ATS-CRM-ENT-003-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#answers
      an empty list for an organization the tenant does not have
  - scenario: ATS-CRM-ENT-004-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#clears
      the field, as every replacement does
  - scenario: ATS-CRM-ENT-004-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#empties
      the collection
  - scenario: ATS-CRM-ENT-004-05
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      the update as ObjectInvalid, naming the element
  - scenario: ATS-CRM-ENT-004-05
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#moves
      the second address into the type the first left, and the preferred flag with it
  - scenario: ATS-CRM-ENT-004-07
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#rolls
      back the contact row the transaction had already updated
  - scenario: ATS-CRM-ENT-004-07
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#leaves
      every row as it was
  - scenario: ATS-CRM-ENT-005-01
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      a fourth e-mail address
  - scenario: ATS-CRM-ENT-005-01
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#stores
      the e-mail address lower-cased with its whitespace removed
  - scenario: ATS-CRM-ENT-005-01
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#drops
      a blank e-mail address
  - scenario: ATS-CRM-ENT-005-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      two numbers of the same type
  - scenario: ATS-CRM-ENT-005-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      two preferred numbers
  - scenario: ATS-CRM-ENT-005-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#formats
      a valid US number, drops a blank one and marks the default preferred number
  - scenario: ATS-CRM-ENT-005-05
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#marks
      the first address of the tenant’s display order as preferred, never one with a seasonal period
  - scenario: ATS-CRM-ENT-005-05
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      a preferred address with a seasonal period
  - scenario: ATS-CRM-ENT-007-01
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      a communication type listed twice, naming the second entry
  - scenario: ATS-CRM-ENT-009-07
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      an affiliation that ends before it starts
  - scenario: ATS-CRM-ENT-009-09
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      to move the affiliation to another organization under the same id (docs/affiliations/README.md §0b.2)
  - scenario: ATS-CRM-ENT-014-02
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#removes
      every owned element with it, the foreign keys cascading
  - scenario: ATS-CRM-ENT-014-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#clears
      the spouse of the contact that named it
  - scenario: ATS-CRM-ENT-014-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#bumps
      the version of that contact and stamps the system, not the caller that deleted (M1-D6)
  - scenario: ATS-CRM-ENT-014-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      the delete as IllegalOperation
  - scenario: ATS-CRM-ENT-014-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#refuses
      the delete as IllegalOperation, naming the element that is locked
  - scenario: ATS-CRM-ENT-014-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js#deletes
      nothing: the contact and every element are still there
  - scenario: ATS-CRM-ENT-003-02
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.queries.integration.spec.js#matches
      "Lovelace, Ada" too: the name is searched in both of the orders a contact is written in
  - scenario: ATS-CRM-ENT-003-02
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.queries.integration.spec.js#answers
      the whole tenant for a search of nothing but spaces: an empty search is no search
  - scenario: ATS-CRM-ENT-003-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.queries.integration.spec.js#answers
      only the contacts with an affiliation to that organization effective today, in number order
  - scenario: ATS-CRM-ENT-003-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.queries.integration.spec.js#answers
      a contact whose affiliation to the organization is not its primary one
  - scenario: ATS-CRM-ENT-003-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.queries.integration.spec.js#leaves
      out a contact whose affiliation to the organization has ended
  - scenario: ATS-CRM-ENT-003-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.queries.integration.spec.js#leaves
      out a contact whose affiliation to the organization begins after today
  - scenario: ATS-CRM-ENT-003-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.queries.integration.spec.js#includes
      a contact whose affiliation in effect to an organization of the walk is not its primary one (M2-D3)
  - scenario: ATS-CRM-ENT-003-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.queries.integration.spec.js#leaves
      out a contact whose affiliation to the organization has not started
  - scenario: ATS-CRM-ENT-003-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.queries.integration.spec.js#leaves
      out a contact whose affiliation to the organization has ended
  - scenario: ATS-CRM-ENT-003-05
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.queries.integration.spec.js#leaves
      out the inactive and the deceased contacts, and keeps the active and the duplicate ones, in number order
  - scenario: ATS-CRM-ENT-001-11
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.spouse.integration.spec.js#links
      the other spouse back when a contact is created naming it
  - scenario: ATS-CRM-ENT-001-11
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.spouse.integration.spec.js#refuses
      to link a contact already recorded as the spouse of a third contact, and writes neither side
  - scenario: ATS-CRM-ENT-001-11
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contacts.spouse.integration.spec.js#clears
      the other spouse when one is deleted
evidence:
  - 'services-crm:modules/entities/tests/integration/db/contact-addresses.table.integration.spec.js'
  - >-
    services-crm:modules/entities/tests/integration/db/contact-communication-preferences.table.integration.spec.js
  - 'services-crm:modules/entities/tests/integration/db/contact-custom-field-values.table.integration.spec.js'
  - 'services-crm:modules/entities/tests/integration/db/contact-email-addresses.table.integration.spec.js'
  - 'services-crm:modules/entities/tests/integration/db/contact-phone-numbers.table.integration.spec.js'
  - 'services-crm:modules/entities/tests/integration/db/contact-roles.table.integration.spec.js'
  - 'services-crm:modules/entities/tests/integration/db/contacts.table.integration.spec.js'
  - 'services-crm:modules/entities/tests/integration/services/contacts/contact-create.integration.spec.js'
  - 'services-crm:modules/entities/tests/integration/services/persistence/contact-roles.integration.spec.js'
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/contact-roles.queries.integration.spec.js
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/contacts-record.queries.integration.spec.js
  - 'services-crm:modules/entities/tests/integration/services/persistence/contacts.archive.integration.spec.js'
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/contacts.conflicts.integration.spec.js
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js
  - 'services-crm:modules/entities/tests/integration/services/persistence/contacts.integration.spec.js'
  - 'services-crm:modules/entities/tests/integration/services/persistence/contacts.queries.integration.spec.js'
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/contacts.seasonal-addresses.integration.spec.js
  - 'services-crm:modules/entities/tests/integration/services/persistence/contacts.spouse.integration.spec.js'
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/endpoint-deletions.integration.spec.js
source_observations:
  - provenance: test
    source: 'services-crm:modules/entities/tests/integration/db/contact-addresses.table.integration.spec.js'
    declared_test_titles:
      - contact_addresses.table (integration)
      - 'the database constraints of crm.contact_addresses, for a writer that goes past the repository'
      - 'accepts a row that breaks no constraint, so the refusals below are the constraints and not the row'
      - refuses a second row with the same tenant and id by contact_addresses_pk
      - refuses a row whose contact the tenant does not have by contact_addresses_contact_fk
      - refuses a row that names this tenant’s contact under another tenant by contact_addresses_contact_fk
      - refuses an address type the tenant does not have by contact_addresses_address_type_fk
      - >-
        accepts an address type of the tenant’s that is marked for organizations, because applies_to is a
        guide
      - refuses a calendar date as a season start by contact_addresses_seasonal_start_check
      - refuses a day the month lacks as a season end by contact_addresses_seasonal_end_check
      - refuses a seasonal preference without a season by contact_addresses_seasonal_preferred_check
      - refuses a preferred address with a seasonal period by contact_addresses_seasonal_not_preferred_check
      - refuses a latitude off the globe by contact_addresses_latitude_check
      - refuses a longitude off the globe by contact_addresses_longitude_check
      - refuses a lock word outside the closed list by contact_addresses_sys_locked_check
      - refuses a second address of the same type by contact_addresses_one_per_type_uq
      - refuses a second preferred address by contact_addresses_one_preferred_uidx
      - 'refuses a row with no address type at all, naming the column'
      - 'refuses a row with no creator, naming the column'
    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/integration/db/contact-communication-preferences.table.integration.spec.js
    declared_test_titles:
      - contact_communication_preferences.table (integration)
      - >-
        the database constraints of crm.contact_communication_preferences, for a writer that goes past the
        repository
      - 'accepts a row that breaks no constraint, so the refusals below are the constraints and not the row'
      - refuses a second row with the same tenant and id by contact_communication_preferences_pk
      - refuses a row whose contact the tenant does not have by contact_communication_preferences_contact_fk
      - >-
        refuses a row that names this tenant’s contact under another tenant by
        contact_communication_preferences_contact_fk
      - refuses a second preference for one communication type by contact_communication_preferences_type_uq
      - >-
        refuses a subscription status outside the closed list by
        contact_communication_preferences_status_check
      - refuses a lock word outside the closed list by contact_communication_preferences_sys_locked_check
      - 'accepts a row with no subscription address: the column is optional'
      - refuses a subscription address with an uppercase letter by the domain’s email_address_canonical_check
      - refuses a subscription address with whitespace by the domain’s email_address_canonical_check
      - >-
        refuses a subscription value that is not shaped like an address by the domain’s
        email_address_format_check
      - 'refuses a row that does not say whether the preference is enabled, naming the column'
      - 'refuses a row with no communication type, naming the column'
    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/integration/db/contact-custom-field-values.table.integration.spec.js'
    declared_test_titles:
      - contact_custom_field_values.table (integration)
      - a value in exactly one of the typed columns
      - held no value for the contact before the write
      - writes one row for the contact and the field
      - keeps the value in the column its data type names
      - 'leaves every other typed column empty, so a reader never has to choose between two'
      - stamps who last changed the value
      - refuses a second row for the same contact and field by contact_custom_field_values_pk
      - 'accepts the same field on another contact, the key being per contact'
      - a long text value
      - saves the value whole
      - 'is found by a case-insensitive equality on the field, as a text filter finds it'
      - the rule that exactly one typed column holds the value
      - refuses a row with no value at all by contact_custom_field_values_one_value_check
      - refuses a row with two values by contact_custom_field_values_one_value_check
      - 'accepts an exact numeric value, because a decimal is never a float here'
      - the rule that a list value holds something
      - 'accepts a list value with one option, so the refusal below is the constraint and not the row'
      - >-
        refuses an empty list, which is an absent value and so no row, by
        contact_custom_field_values_list_check
      - the rule that a value names a custom field key
      - refuses a key without the custom suffix by contact_custom_field_values_custom_field_id_check
      - refuses a key with a service prefix by contact_custom_field_values_custom_field_id_check
      - the contact a value belongs to
      - refuses a value for a contact the tenant does not have by contact_custom_field_values_contact_fk
      - >-
        refuses a value in another tenant naming this tenant's contact by
        contact_custom_field_values_contact_fk
      - deleting the contact
      - held the value while the contact was there
      - 'takes the values with the contact, by the cascade on contact_custom_field_values_contact_fk'
    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/integration/db/contact-email-addresses.table.integration.spec.js'
    declared_test_titles:
      - contact_email_addresses.table (integration)
      - 'the database constraints of crm.contact_email_addresses, for a writer that goes past the repository'
      - 'accepts a row that breaks no constraint, so the refusals below are the constraints and not the row'
      - refuses a second row with the same tenant and id by contact_email_addresses_pk
      - refuses a row whose contact the tenant does not have by contact_email_addresses_contact_fk
      - >-
        refuses a row that names this tenant’s contact under another tenant by
        contact_email_addresses_contact_fk
      - refuses a fourth slot by contact_email_addresses_position_check
      - refuses two addresses in one slot by contact_email_addresses_position_uq
      - refuses a lock word outside the closed list by contact_email_addresses_sys_locked_check
      - refuses an address with an uppercase letter by the domain’s email_address_canonical_check
      - refuses an address with surrounding whitespace by the domain’s email_address_canonical_check
      - refuses an address with whitespace inside it by the domain’s email_address_canonical_check
      - 'refuses an empty address by the column’s NOT NULL, the trigger having made it null'
      - refuses a value that is not shaped like an address by the domain’s email_address_format_check
      - refuses an address whose domain has no dot by the domain’s email_address_format_check
      - 'refuses a row with no address, naming the column'
      - 'refuses a row with no slot, naming the column'
      - contact_email_addresses_primary_email_address_uidx (integration)
      - one primary address per tenant
      - >-
        accepts a primary address no contact of the tenant has, so the refusals below are the index and not
        the row
      - refuses a second contact the same primary address on insert
      - refuses an update that gives another contact’s primary address to this contact’s primary row
      - refuses an update that moves a secondary address into slot 1 when another contact has it as primary
      - a contact rewriting its own primary address
      - 'accepts the same value written again: a contact never conflicts with itself'
      - the same primary address in another tenant
      - 'accepts it: the uniqueness is per tenant'
      - secondary addresses
      - accepts another contact’s primary address as a secondary address (slot 2)
      - accepts another contact’s primary address as a third address (slot 3)
      - accepts one secondary address on two contacts
      - accepts as a primary address one another contact holds only as a secondary
      - contacts without a primary address
      - 'accepts any number of contacts with no address row, the convention for no address'
      - contacts that are not active
      - 'releases the primary address of an inactive contact: another contact may hold it'
      - 'releases the primary address of a deceased contact: another contact may hold it'
      - releases the address of an active contact the moment it becomes inactive
      - keeps the primary address of a contact marked duplicate reserved
      - refuses to make an inactive contact active while another contact holds its primary address
      - refuses to make a deceased contact marked duplicate while another contact holds its primary address
      - leaves the contact as it was when the reactivation is refused
      - makes an inactive contact active again when no other contact holds its primary address
      - 'accepts a change from inactive to deceased while another contact holds the address: neither holds it'
      - >-
        refuses a primary address written for an active contact when an active contact holds it, whatever else
        is inactive
      - >-
        refuses to move a secondary address into slot 1 of an active contact when another active contact has
        it as primary
      - a deleted contact
      - 'frees its primary address at once: its addresses went with it'
      - two writers racing for one primary address
      - makes the second writer wait while the first one’s row is uncommitted
      - refuses the second writer by the index once the first commits
    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/integration/db/contact-phone-numbers.table.integration.spec.js'
    declared_test_titles:
      - contact_phone_numbers.table (integration)
      - 'the database constraints of crm.contact_phone_numbers, for a writer that goes past the repository'
      - 'accepts a row that breaks no constraint, so the refusals below are the constraints and not the row'
      - refuses a second row with the same tenant and id by contact_phone_numbers_pk
      - refuses a row whose contact the tenant does not have by contact_phone_numbers_contact_fk
      - >-
        refuses a row that names this tenant’s contact under another tenant by
        contact_phone_numbers_contact_fk
      - refuses a phone number type the tenant does not have by contact_phone_numbers_phone_number_type_fk
      - >-
        accepts a phone number type of the tenant’s that is marked for organizations, because applies_to is a
        guide
      - refuses a lock word outside the closed list by contact_phone_numbers_sys_locked_check
      - refuses a second number of the same type by contact_phone_numbers_one_per_type_uq
      - refuses a second preferred number by contact_phone_numbers_one_preferred_uidx
      - >-
        refuses an own row with no number, naming the check (only a linked row has none:
        docs/affiliations/README.md §0b.2)
      - 'refuses a row with no phone number type, naming the column'
    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/integration/db/contact-roles.table.integration.spec.js'
    declared_test_titles:
      - contact roles table (integration)
      - constraints
      - 'keys a role by tenant and id: the same id in two tenants, never twice in one'
      - requires a name and whether the role is active
      - 'the id a role may be written under (contact_roles_id_configuration_id_check, migration 0041)'
      - 'takes an id of lowercase letters, digits and single underscores'
      - refuses an id with a capital letter
      - refuses an id with a hyphen
      - refuses an id with a space
      - refuses an id that starts with a digit
      - refuses an id that ends with an underscore
      - refuses an id with two underscores in a row
      - refuses an empty id
      - refuses an id longer than a PostgreSQL identifier
      - takes an id exactly as long as a PostgreSQL identifier
      - 'refuses an id that would read as something else in an API: one of the reserved words'
      - refuses an id under the prefix Rhythm keeps for the roles it ships itself
      - takes an id that merely begins with the letters of that prefix
      - 'refuses the short base58 id the legacy library generated, which is what this replaces'
      - 'the name a role must have (contact_roles_name_not_blank_check, migration 0045)'
      - refuses a name that is the empty string
      - refuses a name that is nothing but spaces
      - 'refuses a name of nothing but a tab and a newline, which the trigger trims as JavaScript does'
      - writes no row at all for a name it refused
      - takes a name with spaces around it and stores it trimmed
      - refuses an update that blanks the name of a role that has one
      - the in-use foreign key (contact_organization_roles_contact_role_fk)
      - 'refuses deleting a role a contact of the tenant holds on an affiliation, as a foreign key violation'
      - deletes a role only another tenant's contact names by the same id
    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/integration/db/contacts.table.integration.spec.js'
    declared_test_titles:
      - contacts.table (integration)
      - 'the database constraints of crm.contacts, for a writer that goes past the repository'
      - 'accepts a row that breaks no constraint, so the refusals below are the constraints and not the row'
      - refuses a second row with the same tenant and id by contacts_pk
      - refuses a second row with the same tenant and contact number by contacts_number_uq
      - refuses a spouse the tenant does not have by contacts_spouse_fk
      - refuses a status outside the closed list by contacts_status_check
      - refuses a gender outside the closed list by contacts_gender_check
      - refuses a contact that is its own spouse by contacts_spouse_not_self_check
      - refuses a contact number that is not positive by contacts_number_check
      - refuses a lock word outside the closed list by contacts_sys_locked_check
      - refuses a row with neither a first nor a last name by contacts_first_or_last_name_check
      - refuses a row whose first and last names are white space only by contacts_first_or_last_name_check
      - accepts a row with a first name alone
      - 'refuses a row with no contact number, naming the column'
      - 'refuses a row with no creator, naming the column'
    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/integration/services/contacts/contact-create.integration.spec.js'
    declared_test_titles:
      - 'the API create of a contact, retried (integration)'
      - the same request sent twice under one UUIDv7
      - makes one contact
      - answers the retry with the contact the first attempt made
      - 'keeps the request’s digest on the row, which is what the retry was compared with'
      - answers no digest on the wire
      - the same request sent twice with its keys in another order
      - >-
        answers the retry with the contact the first attempt made, the digest being of the request and not its
        spelling
      - another request under an id a create already took
      - 'refuses it as a conflict (409), naming the id'
      - leaves the contact the first request made as it was
      - a retry under a UUIDv7 a contact took before it kept a digest
      - 'refuses it as a conflict (409), there being no request to compare it with'
      - 'a retry in a tenant whose block rule, turned on since the first attempt, fires on another contact'
      - has a rule that refuses the retry’s draft as a duplicate of the other contact (premise)
      - 'answers the retry with the contact the first attempt made, rather than the duplicate refusal'
      - makes no second contact
      - a retry of a create with a photo
      - copied the photo for the first attempt (premise)
      - copies nothing for the retry
      - answers the photo the first attempt stored
      - two attempts of the same request that race
      - makes one contact
      - answers both with that contact
      - 'a create under an id that is not a UUIDv7, sent twice'
      - 'refuses the second as a taken id (400), as a client-chosen id always was'
      - an import’s create under a UUIDv7 a contact already has
      - 'refuses it as a taken id (400), an import asking for no replay'
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies and transaction behavior are
      real dependencies. 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/integration/services/persistence/contact-roles.integration.spec.js'
    declared_test_titles:
      - contact roles table (integration)
      - list_automatic_contact_roles
      - 'answers the tenant''s active roles given to every contact or to the first, in id order by bytes'
      - answers nothing of another tenant's roles
      - contact role record (integration)
      - a create with every field and an id
      - stores every field it was sent
      - keeps the id the client named
      - starts the version at 1
      - a create without an id
      - 'gives it the configuration id its NAME suggests, which is what replaced the legacy short base58 id'
      - a create whose id is not a configuration id
      - 'refuses it, naming why, rather than storing the role under an id the client did not send'
      - a create whose name yields no id at all
      - asks for one rather than inventing a generated string nobody chose
      - the name a role must have
      - refuses a create whose name is the empty string
      - refuses a create whose name is nothing but spaces
      - >-
        refuses a create whose name is nothing but a tab and a newline, which the database’s own btrim would
        let through
      - writes no role at all for a create it refused
      - 'refuses the blank name before it asks the name for an id, so the person is told which field is wrong'
      - 'stores a name with spaces around it trimmed, so no two roles differ only by their padding'
      - a replacement whose name is blank
      - is refused as an invalid object
      - 'leaves the role exactly as it was, name, description and version alike'
      - refuses a replacement whose name is the empty string
      - 'answers the database’s own check in the same words, for a writer that reaches it another way'
      - a create at the length limits (CRM-ACC-0173)
      - stores a role with a 63-character id and a 255-character name
      - stores a role whose 255-character name suggests an id cut at the 63-character limit
      - 'refuses a 64-character id as InvalidRequest naming the limit, never an unmapped error'
      - a create with an id the tenant already uses
      - refuses it as an id that already exists
      - the list
      - 'answers the first page by name with case disregarded, so these three are told apart by their ids alone'
      - answers the next page after where the first ended
      - a replacement
      - stores the replaced fields
      - bumps the version
      - a replacement at a version moved on
      - refuses it as stale
      - keeps the first replacement
      - a delete of a role no contact holds
      - removes the role
      - a delete of a role two contacts hold
      - refuses it with the legacy words and the number of holders
      - keeps the role
      - a delete of a role an organization type’s compliance rules name
      - 'refuses it with the legacy words, naming the organization types whose rules depend on it'
      - keeps the role
      - a delete of an unknown role
      - refuses it as not found
      - a delete of a locked role
      - refuses it as an illegal operation
      - keeps the role
      - the place a role holds in the tenant’s list
      - leaves a role created without a display order holding no place at all
      - keeps the place a create does state
      - lets a replacement give a place to a role that held none
      - 'clears the place a replacement is silent about, as it clears any other column'
      - 'refuses a place below 1, by the column’s own check'
      - reorderContactRoles
      - 'writes the tenant’s whole list dense 1..n in the order given, bumping only the roles that moved'
      - 'answers every role in the new order, each whole record as the list maps it'
      - is what the list answers afterwards
      - 'refuses a list that leaves a role of the tenant out, naming the concept and writing nothing'
      - 'refuses a list that names a role only another tenant has, leaving both tenants’ places alone'
      - two tenants
      - lets the second tenant read its own role
      - does not let the tenant `tests` read the second tenant's role
      - does not let the tenant `tests` delete the second tenant's role
    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/integration/services/persistence/contact-roles.queries.integration.spec.js
    declared_test_titles:
      - contact roles read (integration)
      - getContactRole
      - answers the whole record of a role with a value in every column
      - >-
        answers the whole record of a role with nothing but a name and a flag: every empty column an absent
        key
      - answers undefined for an id the tenant does not have
      - 'answers the OTHER tenant’s own role for the shared id, never the first tenant’s record'
      - get_contact_role_fields
      - 'answers the role row alone: every column of the whole record'
      - answers undefined for an id the tenant does not have
      - answers the other tenant’s own row for the shared id
      - listContactRoles
      - 'answers the whole record of every role of the first page, the lower-case name before the capital'
      - 'says where the first page ended, by the place, the name lower-cased and the id of its last role'
      - 'answers the two roles that remain, the second of the pair of equal names first'
      - answers no next position for a last page the limit fills exactly
      - answers an empty page and no next position for a tenant with no roles at all
      - 'leaves out the roles of every other tenant, the first tenant’s whole record among them'
      - list_contact_role_fields
      - 'answers the role rows alone, in name then id order'
      - says nothing about a next position when the limit reached past every role
      - answers the role that follows a position in the middle of a pair of equal names
      - answers no rows and no next position for a position past every name
      - list_automatic_contact_roles
      - >-
        answers only the active roles flagged either way, each with the two flags and the active one, in id
        order
      - leaves out the active role that is flagged neither way
      - leaves out the role that is flagged but no longer active
      - answers nothing in a tenant with no roles at all
      - 'read_contact_role, the narrow read the compliance rule takes'
      - 'answers the role’s id and its name, and nothing else of the row'
      - answers undefined for a role the tenant no longer has
      - answers the other tenant’s own role for the shared id
      - lock_contact_role
      - >-
        answers the role’s row as the TABLE holds it: its tenant column, its unset column null and its
        instants as Postgres sends them
      - 'refuses an id the tenant does not have, by RecordNotFound'
      - 'getContactRolesByIds, the read of one contact role made over a set (integration)'
      - >-
        answers the tenant’s contact roles with those ids in id order, leaving out an id nobody has and
        another tenant’s
      - answers each contact role exactly as the read of that one contact role does
      - answers no contact roles for no ids
    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/integration/services/persistence/contacts-record.queries.integration.spec.js
    declared_test_titles:
      - the whole contact record the contacts persistence reads answer (integration)
      - getContact
      - >-
        gives each of the twenty-seven owned elements a generated id, ten characters or, for an affiliation or
        its role, a UUIDv7, which the literals below then name
      - answers the whole record of a contact with a value in every column and rows in every collection
      - >-
        answers the whole record of a contact with nothing but a last name, its five collections empty and its
        unset columns absent
      - answers undefined for an id the tenant does not have
      - 'answers the other tenant its own contact for an id both tenants use, never this one'
      - listContacts
      - 'answers both contacts of the tenant in contact-number order, each the whole record getContact answers'
      - answers no cursor for a page the limit does not fill
      - list_descendant_contacts
      - answers the one contact of the organization as the whole record getContact answers
      - the custom_fields map of a contact read (integration)
      - >-
        defines all fifteen fields for the tenant, the archived one included, so an absence below belongs to
        the read
      - 'answers one key per live field, each value in the wire form of its data type and the unset one as null'
      - 'leaves the archived field out of the map altogether, where an unset live field is present as null'
      - answers a contact that stored no value at all every live key as null
      - answers the same map through a page as through the single read
    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/integration/services/persistence/contacts.archive.integration.spec.js
    declared_test_titles:
      - archive of a contact (integration)
      - archiving a contact
      - 'sets the instant, the user and the reason'
      - bumps the version once and stamps the caller
      - 'keeps the status it had: archive is not a status'
      - 'keeps every collection and the affiliation, unchanged'
      - is still read by its id
      - 'writes one revision of its own, naming the archive columns and nothing else of the record'
      - a contact that is archived
      - is left out of the list by default
      - is the only kind the list shows for archived=only
      - is listed with the live ones for archived=include
      - refuses a save as RecordArchived
      - is unchanged after a refused save
      - 'is saved by a merge, which names the one save an archived record accepts'
      - 'archived again answers as it is, with no new version'
      - archiving at a version
      - is refused as StaleObject when the contact moved on
      - leaves the contact live after a stale version
      - unarchiving a contact
      - clears the three archive columns
      - 'returns the contact as it was, apart from its version and modified stamps'
      - is back in the default list
      - is saved again
      - is refused as StaleObject at a version it has moved past
      - unarchiving with a reason
      - keeps the trimmed reason as unarchive_reason
      - clears the archive columns as an unarchive without a reason does
      - >-
        writes one revision of its own, naming the archive columns and the reason and nothing else of the
        record
      - shows the reason in that revision as the value it was given
      - is kept by the next read
      - 'is kept through a plain save, which never writes it'
      - is cleared by the next archive
      - 'is left out when no reason, or a blank one, is given'
      - 'is refused as ObjectInvalid over 500 characters, naming the reason'
      - is refused by the table on an archived contact (contacts_unarchive_reason_check)
      - the table
      - refuses an archive instant with no user (contacts_archive_check)
      - refuses a reason on a live contact (contacts_archive_check)
      - refuses a user with no instant (contacts_archive_check)
    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/integration/services/persistence/contacts.conflicts.integration.spec.js
    declared_test_titles:
      - 'code: record_changed'
      - the conflict of a contact save whose version moved on (integration)
      - after a person saved the contact
      - carries the code record_changed
      - >-
        carries the latest change as a user, with the name the history gives them and the instant of the
        revision
      - writes the instant of the change in the form every instant of the API has
      - keeps the message the conflict always had
      - is answered as a 409 whose body holds the code and the latest change
      - after the system changed the contact
      - 'carries the latest change as the system, with the instant and no name'
      - says in the message that the system made the change
      - is still a 409
      - names a person again once a person has saved after the system
      - on the other writes that check the version
      - carries the latest change on an archive at a version that moved on
      - >-
        carries the latest change on an unarchive at a version that moved on, as the system when the system
        moved it
    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/integration/services/persistence/contacts.field-rules.integration.spec.js
    declared_test_titles:
      - what a contact may hold and what a client may send back (integration)
      - an inactive or deceased contact’s primary e-mail address (M1-D1)
      - lets another contact take the primary address of an inactive contact
      - lets another contact take the primary address of a deceased contact
      - 'keeps refusing an address an active contact holds, as the create always did'
      - keeps refusing an address a contact marked duplicate holds
      - releases the address of an active contact that is saved as deceased
      - makes an inactive contact active again when nobody else holds its address
      - >-
        refuses to make an inactive contact active while another contact holds its address, as a conflict
        naming the address
      - leaves the contact inactive and at its version when the reactivation is refused
      - >-
        refuses the reactivation that also gives the contact an address another active contact holds, by the
        ordinary refusal
      - lets a contact change from inactive to deceased while another contact holds its address
      - field limits (M1-D3)
      - accepts a first name of exactly 200 characters
      - 'refuses a first name of 201 characters, naming first_name'
      - 'refuses a last name of 201 characters, naming last_name'
      - 'refuses a middle name of 201 characters, naming middle_name'
      - 'refuses a prefix of 201 characters, naming prefix'
      - 'refuses a suffix of 201 characters, naming suffix'
      - 'refuses a nickname of 201 characters, naming nickname'
      - names every overlong name part of one request at once
      - 'accepts notes of exactly 10,000 characters'
      - 'refuses notes of 10,001 characters, naming notes'
      - 'refuses an e-mail address of 255 characters, naming the element’s email_address'
      - refuses a limit on update as on create
      - 'refuses an unknown country, naming the address’s country'
      - accepts a country by name and stores its code
      - 'accepts an address that names no country, which is the default'
      - computed and internal fields sent back (M1-D5)
      - 'saves a replacement of the whole record read, with the computed full name unchanged'
      - keeps the computed names the server computes when the replacement sent them back
      - 'refuses a replacement whose full name differs from the stored one, naming full_name'
      - 'refuses a replacement whose display name differs from the stored one, naming name'
      - 'refuses a replacement whose sort name differs from the stored one, naming name_last_first'
      - writes nothing when it refuses a changed computed name
      - 'refuses a replacement whose contact number differs from the stored one, naming number'
      - 'refuses a replacement whose creator differs from the stored one, naming sys_created_by_id'
      - 'refuses a replacement whose creation instant differs from the stored one, naming sys_created_at'
      - >-
        answers a stale replacement with the conflict, not with a refusal of a computed name the other save
        changed
      - saves a patch that restates the full name as stored and changes a phone number
      - refuses a patch that changes the full name
      - organizations-only types on a contact (M1-D8)
      - 'refuses a new address of an address type marked for organizations only, naming its type field'
      - 'refuses a new phone number of a phone number type marked for organizations only, naming its type field'
      - lists the organizations-only type with the save’s other problems
      - accepts an address type marked for contacts
      - accepts a phone number type marked for both
      - refuses an update that adds an address of an organizations-only type
      - refuses an update that moves an existing address to an organizations-only type
      - leaves a contact saveable after the type of an address it holds became organizations-only
    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/integration/services/persistence/contacts.integration.spec.js'
    declared_test_titles:
      - contacts.table (integration)
      - insert_contact
      - writes the row into the tenant of the execution
      - takes the tenant's next number from platform.number_seeds
      - advances the tenant number seed by the one number it took
      - computes the display names from the name parts
      - stamps the new row with version 1 and the caller
      - reads the written row back from the database exactly as it answered it
      - 'gives a contact the client named no id a UUIDv7, never its number'
      - still takes the number from the seed for a contact it names itself
      - 'keeps the id the client named, which is no UUIDv7'
      - gives two contacts the client named no id two different ids
      - numbers each tenant on its own
      - 'keeps the number consumed when the transaction rolls back: a gap, never given back'
      - lets another create in the tenant take a number while this create's transaction is still open
      - 'refuses an id the tenant already uses, by contacts_pk'
      - answers the duplicate id as InvalidRequest through contact_refusal
      - 'refuses a spouse that is a contact of another tenant, by contacts_spouse_fk'
      - answers a spouse of another tenant as ForeignKeyViolation through contact_refusal
      - refuses an execution that names no tenant
      - refuses an execution whose caller cannot reach the tenant
      - 'insert_contact: a number another row already holds'
      - a create that meets the taken number 2000 while the highest is 4000
      - 'saves the contact under the highest number plus one, 4001'
      - 'gives it a UUIDv7 id, which the renumbering never touches'
      - 'leaves 4002 in the seed, the next available number'
      - gives the next create 4002
      - leaves the contact that held 2000 exactly as it was
      - reads the saved contact back under 4001
      - a tenant whose numbers run higher than this tenant's
      - renumbers after this tenant's highest number only
      - leaves the other tenant's seed where it was
      - concurrent creates on independent connections that all meet a taken number
      - gives every create a distinct number after the highest
      - leaves the next available number in the seed
      - keeps the seed usable for the next create
      - a normal create while a recovered create's transaction is still open
      - gives the recovering create 4001
      - 'gives the normal create the number after it without waiting, never the stale seed'
      - leaves no number held twice
      - never moves the seed backward
      - a recovery whose transaction then fails
      - propagates the transaction's own failure
      - leaves no contact behind
      - 'keeps the seed the recovery committed in its own transaction, 4002: 2000 and 4001 are gaps'
      - 'gives the next create 4002, with no collision to recover from'
      - a contact the client named after the number the create is about to take
      - 'takes the free number 2000, with no renumbering: the id cannot collide with the named contact'
      - 'gives it a UUIDv7 id of its own, not the string "2000"'
      - leaves the seed after the number it saved
      - leaves the contact the client named "2000" as it was
      - a refusal that is not the number
      - 'refuses an id the tenant already uses by contacts_pk, without renumbering'
      - 'keeps the number the refused create took consumed, a gap'
      - a number the client supplies
      - 'is replaced by the allocated number, as before: a contact number is always the server''s'
      - get_contact_fields
      - answers the tenant's own row for an id two tenants share
      - answers the other tenant's own row for the same id
      - answers nothing for an id the tenant does not have
      - lock_contact
      - answers the row of the tenant that has it
      - 'refuses the same id in a tenant that does not have it, as RecordNotFound'
      - update_contact_fields
      - stores the fields the replacement named
      - keeps the contact number the insert allocated
      - recomputes the display name from the replaced parts
      - bumps the version and stamps the caller that wrote it
      - clears a field the replacement omits
      - refuses a stale version as StaleObject
      - leaves the row as it was after a stale version
      - refuses a row locked against updates as IllegalOperation
      - leaves the row as it was after a refused update of a locked row
      - list_contact_fields
      - 'answers the first page in number order, at most the limit'
      - answers the rest of the rows after the cursor the first page gave
      - delete_contact_row
      - removes the contact row
      - clears the spouse of the contacts that named it
      - bumps the version of each contact whose spouse it cleared
      - refuses a locked row as IllegalOperation
      - leaves the locked row in place after the refusal
      - contact_email_addresses.table (integration)
      - replace_owned_elements over an empty collection
      - read no e-mail address for the contact before the replacement
      - creates one row per e-mail address the input named
      - 'numbers the slots from one, in the order the input listed them'
      - gives every created e-mail address version 1
      - stamps every created e-mail address with the caller
      - generates a ten-character id for every created e-mail address
      - keeps the tenant and the contact off the element the collection answers
      - 'replace_owned_elements with one element kept by its id, one dropped and one new'
      - 'keeps the e-mail address whose id the input named, with every field as it was'
      - removes the e-mail address the input left out
      - 'creates the e-mail address the input named without an id, in the slot its place in the array gives it'
      - replace_owned_elements with an element edited under its id
      - keeps the e-mail address under the id it was created with
      - bumps the version of the edited e-mail address
      - stores the edited address
      - keeps the creation stamp of the edited e-mail address
      - replace_owned_elements with an empty array
      - held two e-mail addresses before they were removed
      - leaves the contact with no e-mail address at all
      - a new e-mail address that names its own id (D-22)
      - keeps the id the client gave
      - 'refuses an id that is not a UUIDv7, naming the field'
      - >-
        refuses a UUIDv7 that another contact’s e-mail address already has, leaving that e-mail address where
        it is
      - an e-mail-address id that belongs to another contact of the tenant
      - 'refuses the claim as an invalid contact, naming the e-mail address that is not this contact’s'
      - leaves this contact’s e-mail addresses exactly as they were
      - leaves the other contact’s e-mail address where it is
      - the same contact id read and written in another tenant
      - answers no e-mail address for the contact id in a tenant that does not have the contact
      - refuses to write an e-mail address of this tenant while acting in another tenant
      - leaves this tenant’s e-mail address untouched after the other tenant’s refused write
      - an e-mail address locked against updates and deletes
      - 'refuses to change the locked e-mail address, naming it'
      - 'refuses to remove the locked e-mail address, naming it'
      - 'names the locked e-mail address through find_locked_element, so a contact delete can refuse'
      - leaves the locked e-mail address exactly as it was after both refusals
      - the persistence rules the repository applies before the database sees a row
      - drops a blank e-mail address
      - stores an address lower-cased with every whitespace character removed
      - accepts an address Joi’s string().email_address() accepts
      - 'refuses an address Joi’s string().email_address() refuses, naming its slot'
      - 'refuses an address whose top-level domain does not exist, as Joi does'
      - refuses a fourth e-mail address
      - contact_phone_numbers.table (integration)
      - replace_owned_elements over an empty collection
      - read no phone number for the contact before the replacement
      - creates one row per phone number the input named
      - gives every created phone number version 1
      - stamps every created phone number with the caller
      - generates a ten-character id for every created phone number
      - keeps the tenant and the contact off the element the collection answers
      - 'replace_owned_elements with one element kept by its id, one dropped and one new'
      - 'keeps the phone number whose id the input named, with every field as it was'
      - removes the phone number the input left out
      - creates the phone number the input named without an id
      - replace_owned_elements with an element edited under its id
      - keeps the phone number under the id it was created with
      - bumps the version of the edited phone number
      - stores the edited values
      - keeps the creation stamp of the edited phone number
      - replace_owned_elements with an empty array
      - held two phone numbers before they were removed
      - leaves the contact with no phone number at all
      - a new phone number that names its own id (D-22)
      - keeps the id the client gave
      - 'refuses an id that is not a UUIDv7, naming the field'
      - >-
        refuses a UUIDv7 that another contact’s phone number already has, leaving that phone number where it
        is
      - a phone-number id that belongs to another contact of the tenant
      - 'refuses the claim as an invalid contact, naming the phone number that is not this contact’s'
      - leaves this contact’s phone numbers exactly as they were
      - leaves the other contact’s phone number where it is
      - the same contact id read and written in another tenant
      - answers no phone number for the contact id in a tenant that does not have the contact
      - refuses to write a phone number of this tenant while acting in another tenant
      - leaves this tenant’s phone number untouched after the other tenant’s refused write
      - a phone number locked against updates and deletes
      - 'refuses to change the locked phone number, naming it'
      - 'refuses to remove the locked phone number, naming it'
      - 'names the locked phone number through find_locked_element, so a contact delete can refuse'
      - leaves the locked phone number exactly as it was after both refusals
      - the persistence rules the repository applies before the database sees a row
      - 'formats a valid US number, drops a blank one and marks the default preferred number'
      - refuses two numbers of the same type
      - refuses two preferred numbers
      - contact_addresses.table (integration)
      - replace_owned_elements over an empty collection
      - read no address for the contact before the replacement
      - creates one row per address the input named
      - gives every created address version 1
      - stamps every created address with the caller
      - generates a ten-character id for every created address
      - keeps the tenant and the contact off the element the collection answers
      - 'replace_owned_elements with one element kept by its id, one dropped and one new'
      - 'keeps the address whose id the input named, with every field as it was'
      - removes the address the input left out
      - creates the address the input named without an id
      - replace_owned_elements with an element edited under its id
      - keeps the address under the id it was created with
      - bumps the version of the edited address
      - stores the edited values
      - keeps the creation stamp of the edited address
      - replace_owned_elements with an empty array
      - held two addresses before they were removed
      - leaves the contact with no address at all
      - a new address that names its own id (D-22)
      - keeps the id the client gave
      - 'refuses an id that is not a UUIDv7, naming the field'
      - 'refuses a UUIDv7 that another contact’s address already has, leaving that address where it is'
      - an address id that belongs to another contact of the tenant
      - 'refuses the claim as an invalid contact, naming the address that is not this contact’s'
      - leaves this contact’s addresses exactly as they were
      - leaves the other contact’s address where it is
      - the same contact id read and written in another tenant
      - answers no address for the contact id in a tenant that does not have the contact
      - refuses to write an address of this tenant while acting in another tenant
      - leaves this tenant’s address untouched after the other tenant’s refused write
      - an address locked against updates and deletes
      - 'refuses to change the locked address, naming it'
      - 'refuses to remove the locked address, naming it'
      - 'names the locked address through find_locked_element, so a contact delete can refuse'
      - leaves the locked address exactly as it was after both refusals
      - the persistence rules the repository applies before the database sees a row
      - 'marks the first address of the tenant’s display order as preferred, never one with a seasonal period'
      - 'accepts a seasonal period that ends before it starts, as a season that crosses the new year'
      - refuses a preferred address with a seasonal period
      - 'refuses a latitude outside -90..90, naming the field'
      - 'refuses a longitude outside -180..180, naming the field'
      - accepts the bounds of both ranges
      - contact_communication_preferences.table (integration)
      - replace_owned_elements over an empty collection
      - read no communication preference for the contact before the replacement
      - 'creates one row per preference the input named, in communication-type order'
      - stores the subscription each preference was written with
      - gives every created preference version 1
      - stamps every created preference with the caller
      - generates a ten-character id for every created preference
      - keeps the tenant and the contact off the element the collection answers
      - 'replace_owned_elements with one element kept by its id, one dropped and one new'
      - 'keeps the preference whose id the input named, with every field as it was'
      - removes the preference the input left out
      - creates the preference the input named without an id
      - replace_owned_elements with an element edited under its id
      - keeps the preference under the id it was created with
      - bumps the version of the edited preference
      - stores the edited values
      - keeps the creation stamp of the edited preference
      - replace_owned_elements with an empty array
      - held two communication preferences before they were removed
      - leaves the contact with no communication preference at all
      - a preference id that belongs to another contact of the tenant
      - 'refuses the claim as an invalid contact, naming the preference that is not this contact’s'
      - leaves this contact’s preferences exactly as they were
      - leaves the other contact’s preference where it is
      - the same contact id read and written in another tenant
      - answers no preference for the contact id in a tenant that does not have the contact
      - refuses to write a preference of this tenant while acting in another tenant
      - leaves this tenant’s preference untouched after the other tenant’s refused write
      - a communication preference locked against updates and deletes
      - 'refuses to change the locked preference, naming it'
      - 'refuses to remove the locked preference, naming it'
      - 'names the locked preference through find_locked_element, so a contact delete can refuse'
      - leaves the locked preference exactly as it was after both refusals
      - the persistence rules the repository applies before the database sees a row
      - 'refuses a communication type listed twice, naming the second entry'
      - stores a subscription address lower-cased with every whitespace character removed
      - 'drops a blank subscription address: no address'
      - 'refuses a subscription address Joi’s string().email() refuses, naming its entry'
      - contact_organization_affiliations through the contact aggregate (integration)
      - a replacement over an empty collection
      - read no affiliation for the contact before the replacement
      - 'creates one row per affiliation the input named, newest first'
      - 'stores the roles the contact held at that organization, on the affiliation'
      - gives every created affiliation version 1
      - stamps every created affiliation with the caller
      - generates a UUIDv7 id for every created affiliation (docs/affiliations/README.md §3.2)
      - keeps the tenant and the contact off the affiliation the read answers
      - 'a replacement with one affiliation kept by its id, one dropped and one new'
      - 'keeps the affiliation whose id the input named, with every field as it was'
      - removes the affiliation the input left out
      - creates the affiliation the input named without an id
      - a replacement with an affiliation edited under its id
      - keeps the affiliation under the id it was created with
      - bumps the version of the edited affiliation
      - stores the edited values
      - >-
        refuses to move the affiliation to another organization under the same id (docs/affiliations/README.md
        §0b.2)
      - keeps the creation stamp of the edited affiliation
      - a replacement with an empty array
      - held two affiliations before they were removed
      - leaves the contact with no affiliation at all
      - an affiliation id that belongs to another contact of the tenant
      - 'refuses the claim as an invalid contact, naming the affiliation that is not this contact’s'
      - leaves this contact’s affiliations exactly as they were
      - leaves the other contact’s affiliation where it is
      - the same contact id read and written in another tenant
      - answers no affiliation for the contact id in a tenant that does not have the contact
      - >-
        refuses to write an affiliation of this tenant while acting in another tenant, by the contact’s key
        there
      - leaves this tenant’s affiliation untouched after the other tenant’s refused write
      - an affiliation locked against updates and deletes
      - 'refuses to change the locked affiliation, naming it'
      - 'refuses to remove the locked affiliation, naming it'
      - 'names the locked affiliation through locked_element_of, so a contact delete can refuse'
      - leaves the locked affiliation exactly as it was after both refusals
      - the persistence rules the aggregate applies before the database sees a row
      - refuses an affiliation that ends before it starts
      - createContact (integration)
      - a contact created while its number is already taken
      - answers the contact under the highest number plus one
      - 'gives it a UUIDv7 id, which no renumbering changes'
      - persists every owned collection under the renumbered contact
      - 'leaves 4002 in the seed, the next available number'
      - a contact with one of each collection
      - 'answers the contact under the id the client named, in the tenant of the execution'
      - allocates the tenant's next contact number
      - advances the tenant number seed by the one number it took
      - 'stamps the contact with version 1 and the caller, as creator and as last modifier'
      - computes the three names from the parts
      - defaults the flags the legacy record defaulted
      - 'numbers the e-mail addresses from one, in the order they were given'
      - 'stores the phone numbers, the preferred one first'
      - 'stores the addresses in the read order: preferred first, then the tenant’s type display order'
      - keeps the seasonal dates and defaults the country of a seasonal address
      - stores the one communication preference it was given
      - 'stores the one ended affiliation it was given, its Organization History'
      - gives every element a generated id of its own
      - stamps every element with version 1 and the caller
      - 'keeps the tenant and the owner off the wire: they are the key, not the element'
      - 'writes one row per element, across the six owned tables'
      - reads back exactly what the create answered
      - a contact the client gives no id
      - 'gives it a UUIDv7 id (Andrew, 2026-10-10), never its contact number'
      - 'still takes the tenant’s next contact number, which stays its own column'
      - reads the contact back under the id it was given
      - a contact with no collections
      - 'answers an empty array for each collection, never a missing property'
      - writes the contact row and no element row
      - the same contact id created in two tenants
      - creates each contact in the tenant of its execution
      - answers the first tenant its own contact for that id
      - answers the second tenant its own contact for that id
      - a contact married to another contact of the tenant
      - links the spouse
      - an id the tenant already uses
      - refuses the create as InvalidRequest
      - 'leaves the contact number it took consumed, a gap'
      - leaves the contact that holds the id as it was
      - an element that names an id the contact does not have
      - 'refuses the create as ObjectInvalid, naming the element and its property'
      - rolls the contact row back with the rest of the transaction
      - 'leaves the contact number it took consumed, a gap'
      - an element id that belongs to another tenant's contact
      - 'refuses to claim it, as ObjectInvalid'
      - creates nothing
      - an affiliation to an organization the tenant does not have
      - >-
        refuses the create as ObjectInvalid on the row that names it, before the key
        contact_organization_affiliations_organization_fk
      - creates nothing
      - a spouse that is not a contact of the tenant
      - 'refuses the create as ForeignKeyViolation, naming the spouse'
      - creates nothing
      - 'leaves the contact number it took consumed, a gap'
      - a spouse that is another tenant's contact
      - 'refuses the create as ForeignKeyViolation, even though this tenant does not use that id'
      - creates nothing
      - 'a primary e-mail address another contact of the tenant already has, in another case'
      - 'refuses the create as IllegalOperation, in the words of the legacy check'
      - rolls the contact row back with the rest of the transaction
      - 'leaves the contact number it took consumed, a gap'
      - a contact whose e-mail addresses arrive in capitals and with spaces
      - stores the e-mail address lower-cased with its whitespace removed
      - stores the subscription address lower-cased with its whitespace removed
      - a contact with an e-mail address that is not valid
      - 'refuses the create as ObjectInvalid, naming the address'
      - writes nothing
      - a contact that breaks a persistence rule
      - refuses the create as ObjectInvalid
      - 'writes nothing, the rules running before the first insert'
      - takes no contact number
      - getContact (integration)
      - a seeded contact of the tenant `tests`
      - answers the contact fields as the seed wrote them
      - answers that tenant's elements of the collections it has
      - answers an empty array for a collection the contact has no element in
      - the same contact id in another tenant
      - answers that tenant's own contact for the id
      - 'answers that tenant''s own elements, never the first tenant''s'
      - an id the tenant does not have
      - answers undefined
      - 'answers undefined for a tenant that has no contact of that id, even though another tenant has one'
      - a contact with an element in every collection
      - >-
        orders the affiliations: the primary first, then the open ones, then the ended ones, its Organization
        History
      - puts the preferred address first
      - orders the e-mail addresses by their slot
      - listContacts (integration)
      - a page smaller than the tenant has contacts
      - 'answers the first contacts in number order, at most the limit'
      - answers each contact with its collections
      - answers the number of the last contact of the page as the cursor
      - 'answers the contacts that follow the cursor, in number order'
      - answers the contacts that remain on the last page
      - answers no cursor when nothing follows the last page
      - a page as large as the tenant has contacts
      - answers them all
      - 'answers no cursor, a complete page with nothing after it'
      - a tenant whose contacts share their ids with another tenant
      - answers only that tenant's contacts
      - answers no contact of another tenant on a page large enough to hold them
      - a tenant with no contacts at all
      - answers an empty page with no cursor
      - list_descendant_contacts (integration)
      - an organization with two levels of children below it and a parent above it
      - >-
        answers the organization's own contacts, then each child's subtree in name order, contacts by sort
        name (bytewise)
      - keeps an inactive organization and its inactive contact
      - leaves out the contacts of the organization's parent
      - leaves out the contacts of an unrelated organization
      - answers every contact as a record of the tenant
      - the topmost organization of the hierarchy
      - answers all nine contacts below and on it
      - answers each contact once
      - a leaf organization
      - answers only its own contacts
      - nothing to answer
      - answers an empty list for a hierarchy without contacts
      - answers an empty list for an organization the tenant does not have
      - another tenant with the same organization ids
      - answers that tenant's own contacts
      - never answers the other tenant's contact to this tenant
      - answers an empty list in a tenant that has no organizations
      - a contact with owned collections
      - answers the contact with its e-mail addresses
      - answers the contact with every one of its collections
      - a parent cycle (one -> two -> three -> one)
      - 'walks the cycle once from its first organization, missing nothing on it or below it'
      - walks the cycle once from an organization in its middle
      - answers a branch hanging off the cycle with only that branch
      - database round trips
      - answers the lone organization its one contact
      - answers all 85 contacts of the hierarchy
      - sends Postgres the same number of statements for 85 organizations as for one
      - >-
        sends at most eleven statements: begin, the tenant and role, the walk, five collections, the custom
        field definitions and values, commit
      - updateContact (integration)
      - a replacement of the contact fields alone
      - stores the field the replacement changed
      - 'keeps the contact number the create allocated, ignoring the one the client sent'
      - 'recomputes the name itself, ignoring the one the client sent'
      - bumps the version and stamps the caller that wrote it
      - leaves the creator of the row as it was
      - reads back exactly what the update answered
      - a replacement that omits a field
      - 'clears the field, as every replacement does'
      - recomputes the name without it
      - a change to the contact that leaves every element as it was
      - 'bumps the contact''s version, the record''s version being the contact''s'
      - 'leaves the phone numbers as they were, versions included'
      - 'leaves the addresses as they were, versions included'
      - 'a replacement that adds, edits and removes elements at once'
      - 'keeps the edited phone number under its own id, with the new value and a bumped version'
      - removes the phone number the replacement left out
      - 'creates the phone number the replacement named without an id, at version 1'
      - 'gives the new phone number an id of its own, not one it is replacing'
      - removes the address the replacement left out
      - 'renumbers the e-mail addresses from one, in the order the replacement listed them'
      - keeps a moved e-mail address under its own id
      - leaves the database with one row per element the replacement named
      - a replacement that swaps two addresses between their types
      - 'moves the first address into the type the second left, without tripping the one-per-type constraint'
      - 'moves the second address into the type the first left, and the preferred flag with it'
      - a replacement that omits a collection entirely
      - empties the collection
      - deletes the rows of the emptied collection and leaves the others alone
      - bumps the contact's version for a change to a collection alone
      - a contact that moves from one organization to another
      - >-
        keeps no past affiliation when the contact first joins an organization: its one affiliation is the
        current primary
      - 'keeps the organization the contact left as an ended affiliation, beside the new primary'
      - >-
        records the job title held there and its last day, the day before the move
        (docs/affiliations/README.md §5.2)
      - 'keeps the roles held there on the ended affiliation, ended the same day'
      - a replacement sent at a version the contact has moved past
      - refuses the update as StaleObject
      - leaves every row as it was
      - a replacement of an id the tenant does not have
      - refuses the update as RecordNotFound
      - a replacement sent through another tenant
      - 'refuses the update as RecordNotFound: the key is the tenant and the id'
      - leaves the contact of the owning tenant as it was
      - an element id that belongs to another contact of the tenant
      - 'refuses the update as ObjectInvalid, naming the element'
      - rolls back the contact row the transaction had already updated
      - leaves the contact that owns the element with its element
      - an element id that belongs to another tenant's contact
      - 'refuses the update as ObjectInvalid, naming the element'
      - leaves the replaced contact as it was
      - 'leaves the other tenant''s element untouched, at the version it had'
      - the same element listed twice
      - refuses the update as ObjectInvalid
      - leaves every row as it was
      - a contact locked against updates
      - refuses the update as IllegalOperation
      - leaves every row as it was
      - an element locked against updates and one locked against deletes
      - 'refuses a change to the element locked against updates, as IllegalOperation'
      - leaves every row as it was after the refused change
      - 'refuses the removal of the element locked against deletes, as IllegalOperation'
      - leaves every row as it was after the refused removal
      - accepts a change to the contact that leaves both locked elements as they are
      - a replacement that gives the contact the primary e-mail address another contact of the tenant has
      - 'refuses the replacement as IllegalOperation, in the words of the legacy check'
      - leaves every row of the contact as it was
      - a replacement that keeps the primary e-mail address and adds a secondary one
      - 'saves the contact: its own primary address never conflicts with itself'
      - deleteContact (integration)
      - a contact with one of each collection
      - answers the id it deleted
      - leaves no contact to read
      - 'removes every owned element with it, the foreign keys cascading'
      - a contact that another contact names as its spouse
      - clears the spouse of the contact that named it
      - 'bumps the version of that contact and stamps the system, not the caller that deleted (M1-D6)'
      - the same contact id in two tenants
      - leaves the other tenant's contact exactly as it was
      - leaves the other tenant's owned elements in place
      - an id the tenant does not have
      - refuses the delete as RecordNotFound
      - an id only another tenant has
      - 'refuses the delete as RecordNotFound: the key is the tenant and the id'
      - leaves the other tenant's contact in place
      - a contact locked against deletes
      - refuses the delete as IllegalOperation
      - leaves the contact exactly as it was
      - a contact one of whose elements is locked against deletes
      - 'refuses the delete as IllegalOperation, naming the element that is locked'
      - 'deletes nothing: the contact and every element are still there'
      - contact persistence rules (integration)
      - 'normalisation, as stored'
      - 'moves an honorific salutation to the prefix, start-cased and dotted (RB-2367)'
      - builds the full name from the prefix it moved
      - leaves no salutation behind once it has become the prefix
      - keeps a salutation that is not an honorific
      - 'drops a nickname equal to the first name, ignoring case (RS-2647)'
      - >-
        refuses the same role twice over one period of an affiliation, naming the second (D6,
        docs/affiliations/README.md §3.3)
      - drops blank e-mail addresses and numbers the rest from one (RB-1699)
      - drops blank phone numbers and formats the US numbers that remain (RS-2635)
      - >-
        picks the preferred phone number in the legacy order when none is marked: mobile, work, home, school,
        others
      - 'picks the preferred address in the legacy order when none is marked: work, home, school, others'
      - 'never picks an address with a seasonal period as the preferred one, even when it is the only address'
      - keeps the preferred element the client marked
      - a contact that is its own spouse
      - 'refuses the create as ObjectInvalid, naming the problem'
      - creates no contact when it refuses the create
      - takes no contact number when it refuses the create
      - 'refuses the same change as an update, as ObjectInvalid'
      - leaves the contact and its collections as they were after the refused update
      - more than three e-mail addresses
      - 'refuses the create as ObjectInvalid, naming the problem'
      - creates no contact when it refuses the create
      - takes no contact number when it refuses the create
      - 'refuses the same change as an update, as ObjectInvalid'
      - leaves the contact and its collections as they were after the refused update
      - two phone numbers in the same fixed slot
      - 'refuses the create as ObjectInvalid, naming the problem'
      - creates no contact when it refuses the create
      - takes no contact number when it refuses the create
      - 'refuses the same change as an update, as ObjectInvalid'
      - leaves the contact and its collections as they were after the refused update
      - a phone number with no type at all
      - 'refuses the create as ObjectInvalid, naming the problem'
      - creates no contact when it refuses the create
      - takes no contact number when it refuses the create
      - 'refuses the same change as an update, as ObjectInvalid'
      - leaves the contact and its collections as they were after the refused update
      - a phone number type the tenant does not have
      - 'refuses the create as ObjectInvalid, naming the problem'
      - creates no contact when it refuses the create
      - 'leaves the contact number it took consumed, a gap, the key refusing inside the transaction'
      - 'refuses the same change as an update, as ObjectInvalid'
      - leaves the contact and its collections as they were after the refused update
      - two preferred phone numbers
      - 'refuses the create as ObjectInvalid, naming the problem'
      - creates no contact when it refuses the create
      - takes no contact number when it refuses the create
      - 'refuses the same change as an update, as ObjectInvalid'
      - leaves the contact and its collections as they were after the refused update
      - two addresses in the same fixed slot
      - 'refuses the create as ObjectInvalid, naming the problem'
      - creates no contact when it refuses the create
      - takes no contact number when it refuses the create
      - 'refuses the same change as an update, as ObjectInvalid'
      - leaves the contact and its collections as they were after the refused update
      - two addresses of the same tenant-defined type
      - 'refuses the create as ObjectInvalid, naming the problem'
      - creates no contact when it refuses the create
      - takes no contact number when it refuses the create
      - 'refuses the same change as an update, as ObjectInvalid'
      - leaves the contact and its collections as they were after the refused update
      - two phone numbers of the same tenant-defined type
      - 'refuses the create as ObjectInvalid, naming the problem'
      - creates no contact when it refuses the create
      - takes no contact number when it refuses the create
      - 'refuses the same change as an update, as ObjectInvalid'
      - leaves the contact and its collections as they were after the refused update
      - an address with no type at all
      - 'refuses the create as ObjectInvalid, naming the problem'
      - creates no contact when it refuses the create
      - takes no contact number when it refuses the create
      - 'refuses the same change as an update, as ObjectInvalid'
      - leaves the contact and its collections as they were after the refused update
      - two preferred addresses
      - 'refuses the create as ObjectInvalid, naming the problem'
      - creates no contact when it refuses the create
      - takes no contact number when it refuses the create
      - 'refuses the same change as an update, as ObjectInvalid'
      - leaves the contact and its collections as they were after the refused update
      - a seasonal period with a start and no end
      - 'refuses the create as ObjectInvalid, naming the problem'
      - creates no contact when it refuses the create
      - takes no contact number when it refuses the create
      - 'refuses the same change as an update, as ObjectInvalid'
      - leaves the contact and its collections as they were after the refused update
      - two seasonal addresses whose seasons overlap
      - 'refuses the create as ObjectInvalid, naming both addresses'
      - creates no contact when it refuses the create
      - 'refuses the same change as an update, as ObjectInvalid'
      - a seasonal period written as a calendar date
      - 'refuses the create as ObjectInvalid, naming the field'
      - a preferred address with a seasonal period
      - 'refuses the create as ObjectInvalid, naming the problem'
      - creates no contact when it refuses the create
      - takes no contact number when it refuses the create
      - 'refuses the same change as an update, as ObjectInvalid'
      - leaves the contact and its collections as they were after the refused update
      - an address whose latitude is above 90
      - 'refuses the create as ObjectInvalid, naming the field'
      - creates no contact when it refuses the create
      - takes no contact number when it refuses the create
      - 'refuses the same change as an update, as ObjectInvalid naming the field'
      - leaves the contact and its collections as they were after the refused update
      - an address whose longitude is below -180
      - 'refuses the create as ObjectInvalid, naming the field'
      - creates no contact when it refuses the create
      - takes no contact number when it refuses the create
      - 'refuses the same change as an update, as ObjectInvalid naming the field'
      - leaves the contact and its collections as they were after the refused update
      - a communication type listed twice
      - 'refuses the create as ObjectInvalid, naming the problem'
      - creates no contact when it refuses the create
      - takes no contact number when it refuses the create
      - 'refuses the same change as an update, as ObjectInvalid'
      - leaves the contact and its collections as they were after the refused update
      - an affiliation that ends before it starts
      - 'refuses the create as ObjectInvalid, naming the problem'
      - creates no contact when it refuses the create
      - takes no contact number when it refuses the create
      - 'refuses the same change as an update, as ObjectInvalid'
      - leaves the contact and its collections as they were after the refused update
      - a contact that breaks two rules at once
      - lists every problem at once
      - 'the database refuses what the rules refuse, for a writer that goes around them'
      - >-
        accepts a seasonal address with no dates that is not preferred, so the refusals below are the
        constraints and not the row
      - leaves the contact with the one phone number the repository wrote
      - refuses a second phone number of the home type by contact_phone_numbers_one_per_type_uq
      - refuses a second preferred phone number by contact_phone_numbers_one_preferred_uidx
      - refuses a phone number type the tenant does not have by contact_phone_numbers_phone_number_type_fk
      - refuses a second address of the home type by contact_addresses_one_per_type_uq
      - refuses a calendar date as a season start by contact_addresses_seasonal_start_check
      - refuses a fourth e-mail position by contact_email_addresses_position_check
      - refuses an element of a contact the tenant does not have by contact_phone_numbers_contact_fk
      - 'refuses a spouse in another tenant by contacts_spouse_fk, even when the other tenant''s contact exists'
      - 'one entry of each type, enforced by the database too'
      - >-
        holds the one pager number the repository wrote, so the refusal below is the constraint and not the
        row
      - >-
        accepts the first school address written past the repository, so the refusal below is the constraint
        and not the row
      - refuses a second address of the same type by contact_addresses_one_per_type_uq
      - refuses a second number of the same type by contact_phone_numbers_one_per_type_uq
      - 'accepts an address of another type, because the constraint is per type'
      - contact custom fields (integration)
      - a create refused for one invalid custom field
      - 'refuses it, naming the field'
      - leaves no contact and none of its collections
      - leaves none of its valid custom field values either
      - an update whose custom field fails after the row was written
      - 'leaves the standard field as it was, the row update rolled back with the refusal'
      - leaves the other custom field as it was
      - leaves the version as it was
      - two writers that each change a different custom field
      - lets exactly one of them write
      - refuses the other as stale rather than overwriting the first
      - keeps the field the refused writer did not change at the winner's value
      - keeps both changes once the refused writer retries on the version it reads again
      - an update that names one custom field
      - stores false as a value row
      - does not rewrite the row of the field it did not name
      - a page of contacts with values
      - 'answers twenty contacts, each with its custom fields'
      - sends the same number of statements for twenty contacts as for one
      - an archived field
      - is not answered while archived
      - keeps its value row while archived
      - answers the kept value again once restored
      - tenants with a field of the same key
      - validates each tenant's value against its own definition
      - refuses in one tenant a value only the other tenant's definition accepts
      - answers each tenant's contact only its own tenant's fields
      - contact photo through the asset pipeline (integration)
      - a create whose photo is a staging upload
      - 'stores the permanent reference the legacy console reads, in the record’s own folder'
      - reads back exactly the reference the create answered
      - writes the object into permanent storage with the type its bytes showed
      - 'copies the bytes themselves, not only the record’s reference'
      - 'serves the file under the name the person uploaded, made safe for a key and a header'
      - 'copies exactly the version it inspected, so an upload replaced mid-save is never copied'
      - 'leaves the upload in the staging area, as Rhythm 1.0 left it'
      - >-
        attaches the lifecycle row to the contact, naming the object, the field and the source it was copied
        from
      - writes exactly one lifecycle row for the photo
      - a create that lets the server name the contact
      - 'gives the contact a UUIDv7 id, the same form a create with no file to copy gets'
      - 'files the photo under the contact’s own id, allocated before the copy'
      - 'files it under no operation folder at all, the record having had an id before the first copy'
      - attaches the row to the contact the photo was filed under
      - 'still takes the contact’s number from the tenant’s seed, which is no longer its id'
      - a create whose photo is an external URL
      - 'imports the file and stores the permanent reference, not the URL it came from'
      - 'streams the file into storage, there being nothing in the bucket to copy'
      - 'stores the type the bytes showed, never the Content-Type the server declared'
      - records the file as imported from the web and attaches it to the contact
      - a create whose photo is a file already stored for the tenant
      - 'stores the reference exactly as it stands, the file already being permanent'
      - inspects and sniffs the file but copies nothing and uploads nothing
      - writes a lifecycle row for the contact whose file it did copy
      - 'writes no lifecycle row for the reused file, which this pipeline did not write'
      - a create whose photo is a legacy delivery link
      - 'canonicalises the link to the service reference the record stores, dropping the access token'
      - 'copies nothing, the link naming a file the store already holds'
      - a create naming an upload that is not there
      - 'refuses the create as ObjectInvalid, naming the field and the file'
      - creates no contact at all
      - a create naming another tenant's upload
      - 'refuses the create as ObjectInvalid, the tenant of the key not being the caller’s'
      - creates no contact at all
      - a create whose upload is not an image
      - 'refuses the create as ObjectInvalid, the bytes deciding and not the name'
      - creates no contact at all
      - a create naming a URL whose address may not be fetched
      - 'refuses the create as ObjectInvalid, the server never fetching an internal address for a caller'
      - creates no contact at all
      - an update naming an upload that is not there
      - 'refuses the update as ObjectInvalid, naming the field'
      - leaves the contact holding the photo it had
      - leaves the object of that photo in storage
      - leaves the lifecycle row of that photo attached
      - a patch that does not mention the photo
      - copied the upload when the photo was first set
      - 'asks storage for nothing at all, an unrelated edit never migrating a file'
      - keeps the photo the patch does not mention
      - 'leaves the lifecycle row attached, the file still being the contact’s'
      - applies the edit the patch did name
      - a patch that removes the photo
      - clears the field
      - 'keeps the file itself, as Rhythm 1.0 kept a file a record stopped referencing'
      - detaches the lifecycle row of the file the contact no longer references
      - 'copies nothing, a removal never touching storage'
      - a patch that sends the photo back unchanged
      - keeps the reference it already held
      - 'asks storage for nothing, an equal value never being classified, fetched or verified'
      - 'leaves the lifecycle row attached, with nothing detached beside it'
      - a replacement of the photo by another upload
      - 'stores a new reference at a key of its own, never the one the old file has'
      - reads back the new reference
      - keeps the file the previous version of the record references
      - stores the new object with the type its own bytes showed
      - 'detaches the replaced file’s row and attaches the new one, in the record’s own transaction'
      - a save whose database write fails after the file was copied
      - refuses the save as ForeignKeyViolation
      - leaves the contact holding the photo it had
      - 'leaves the old file’s row attached, the commit never having happened'
      - 'abandons the row of the file it had already copied, for cleanup to settle'
      - 'deletes nothing inside the request: the copied object waits for the cleanup pass'
      - 'the same save sent twice, its first response lost'
      - answers the same permanent reference both times
      - 'copies the upload exactly once, the retry reusing what the first attempt prepared'
      - leaves the contact holding that reference
      - 'leaves exactly one lifecycle row, attached'
      - two saves replacing the photo at once
      - lets exactly one of the two saves commit
      - 'refuses the save that lost as StaleObject, asking the client to read the record again'
      - 'leaves the contact holding the winner’s file, each save having copied to a key of its own'
      - attaches the winner’s row to the contact
      - 'abandons the loser’s row, its file referenced by nothing'
      - the cleanup that follows
      - 'claims the loser’s row and never the winner’s, among this contact’s rows'
      - deletes the loser’s file
      - marks the loser’s row discarded
      - 'keeps the winner’s file, whose row cleanup may not claim'
      - leaves the winner’s row attached
      - a write of the photo that did not go through the pipeline
      - 'refuses a direct insert_contact that assigns a file reference, as ObjectInvalid'
      - creates no contact for the refused insert
      - 'refuses a direct update_contact_fields that changes the file reference, as ObjectInvalid'
      - leaves the contact holding the photo the pipeline did save
      - preparing two files of which one cannot be saved
      - 'refuses the preparation as ObjectInvalid, naming the custom field that failed'
      - 'abandons the row of the file it did copy, so neither field is half-saved'
      - attaches nothing of the failed preparation
      - leaves the contact holding the photo it had
      - an image custom field saved through the same pipeline
      - stores the permanent reference in the value’s fileUrlValue
      - attaches the lifecycle row to the contact under the custom field’s path
      - answers the cleanup that a custom value still references the file
      - answers the cleanup that a file no contact holds is referenced by nothing
      - address country and state (integration)
      - a contact created with countries and states named in words
      - stores the state and country codes of the home address
      - 'stores a province the rule does not know as sent, beside the country code'
      - stores the default country for an address sent without one
      - a contact replacement that changes an address to a country named in words
      - stores the country code
      - an organization created with a country and state named in words
      - stores the state and country codes
      - the primary organization through the contact saves (integration)
      - all or nothing
      - a save rolled back after the move leaves the primary organization and the affiliations as they were
      - >-
        a save refused after the contact row was updated (an element of another contact) leaves the primary
        organization as it was
      - concurrency
      - 'two saves of one contact at the same version: one wins, and only its affiliations are stored'
      - 'successive saves at each new version all apply, each ending the organization the one before set'
      - the affiliations' dates and limit
      - >-
        ends the organization left the day before the tenant's date, not the server's: 02:00 UTC on the 9th is
        still the 8th in Los Angeles
      - >-
        keeps no affiliation for a stay that would end before it began: moves back and forth on one day leave
        only the stay before it
      - >-
        refuses a client that sends 101 affiliations, a contact holding at most 100
        (docs/affiliations/README.md §3.4)
      - a contact and its contact type (integration)
      - a create
      - leaves the contact with no type when the input names none
      - stores nothing in the column for a contact created with no type
      - gives the contact the type the input names
      - 'accepts a type the tenant has switched off, which is a type like any other here'
      - 'refuses a type the tenant does not have, naming it'
      - 'refuses another tenant''s type, which this tenant does not have either'
      - a replacement
      - gives the contact the type the replacement names
      - stores it in the column
      - changes the type a contact already had to the one the replacement names
      - 'clears the type when the replacement states none, as a replacement clears everything it omits'
      - 'refuses a type the tenant does not have, and names it'
      - leaves the type a contact had when a replacement naming another tenant's type is refused
      - 'contact custom fields: an archived option (integration)'
      - holds gold on the contact created while it was offered
      - 'refuses a create that chooses the archived option, naming the field'
      - saves an update that re-sends the archived option the contact already holds
      - 'refuses an update that newly chooses the archived option, naming the field'
      - keeps the value of the contact whose update was refused
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies and transaction behavior are
      real dependencies. 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/integration/services/persistence/contacts.queries.integration.spec.js
    declared_test_titles:
      - a contact read with its contact type (integration)
      - getContact
      - answers the whole record of a contact of the first type
      - answers the whole record of a contact of the second type
      - >-
        answers the whole record of a contact with no type, which carries no type property at all (an absent
        column is an absent property)
      - listContacts
      - 'answers the whole record of every contact of the page, each with the type it has'
      - the contact row reads and their paging (integration)
      - get_contact_fields
      - 'answers the whole contact row, with none of the collections the record read adds'
      - answers undefined for an id the tenant does not have
      - answers undefined for a contact that belongs to another tenant
      - list_contact_fields
      - 'answers the first page whole, in contact-number order, and the number to come back with'
      - 'answers the next page whole, beginning after the number the first page named'
      - 'answers a last page the limit fills exactly with no cursor at all, because no row follows it'
      - answers the whole tenant with no cursor when the limit is exactly the number of contacts it has
      - answers an empty page beginning past the last contact number of the tenant
      - answers an empty page in a tenant that has no contacts at all
      - never answers a contact of another tenant that holds the very same contact numbers
      - listContacts
      - >-
        answers the same page as list_contact_fields, each row given its five empty collections and its custom
        fields
      - answers an empty list and no cursor in a tenant that has no contacts at all
      - list_contact_fields_by_ids
      - 'answers the whole row of each id it was given, in byte order of the id rather than the order asked for'
      - leaves out an id the tenant does not have rather than answering a gap for it
      - answers an empty list when it is given no ids at all
      - never answers another tenant's contact for an id only that tenant has
      - the contacts list narrowed by organization and by name (integration)
      - >-
        answers every contact of the tenant when neither filter is stated, so each case below is about the
        filter
      - organization_id
      - 'holds four affiliations to the choir: two current, one ended and one not yet begun (premise)'
      - 'answers only the contacts with an affiliation to that organization effective today, in number order'
      - answers a contact whose affiliation to the organization is not its primary one
      - leaves out the contact that belongs to no organization at all
      - leaves out a contact whose affiliation to the organization has ended
      - leaves out a contact whose affiliation to the organization begins after today
      - 'answers an empty page for an organization the tenant does not have, rather than every contact'
      - >-
        narrows the page rather than the tenant: another tenant’s contact of the same organization id is not
        reached
      - 'pages within the filter: the limit and the cursor count only the contacts it answers'
      - search
      - answers the contacts whose name contains it
      - 'matches whatever the case, so a person typing in lower case finds the same contacts'
      - matches a prefix of the name
      - 'matches the first name as well as the last, because it searches the whole name'
      - 'matches "Lovelace, Ada" too: the name is searched in both of the orders a contact is written in'
      - takes a percent sign as a character of the name and not as a wildcard
      - takes an underscore as a character of the name and not as a single-character wildcard
      - 'answers the whole tenant for a search of nothing but spaces: an empty search is no search'
      - reaches no contact of another tenant that carries the very same name
      - 'applies both filters together, each narrowing the other'
      - 'narrows the contact RECORD list the same way, collections and all'
      - the descendant contacts of an organization (integration)
      - list_descendant_contact_fields
      - >-
        answers the organization its own contacts first, then each child subtree in organization-name order,
        depth first
      - 'orders two contacts of one organization that share a name by their id, bytewise'
      - 'answers each contact exactly once, however many organizations the walk passed through'
      - answers the whole row of a contact two levels below the organization
      - 'keeps an inactive contact of an inactive organization, and answers its whole row'
      - >-
        includes a contact whose affiliation in effect to an organization of the walk is not its primary one
        (M2-D3)
      - 'answers a contact with ties to two organizations of the walk once, at the first of them in the order'
      - leaves out a contact whose affiliation to the organization has not started
      - leaves out a contact whose affiliation to the organization has ended
      - answers an empty list for an organization the tenant does not have
      - never answers another tenant's contact under an organization id both tenants use
      - list_descendant_contacts
      - answers the same contacts in the same order as the row read it is built on
      - gives the contact of the walk its affiliation under a generated id (premise)
      - >-
        gives each contact of the walk its collections, empty where it has no rows, its affiliation and its
        primary organization
      - what the region assignment reads of a contact (integration)
      - lock_contact_region_targets
      - >-
        answers each contact it may write with its region, its override flag and its fingerprint, a contact
        with none of them as null
      - 'answers the deciding address the last assignment stored, which is what a repeat compares against'
      - 'leaves out a contact locked against updates, which the assignment may not write'
      - answers an empty list when it is given no ids at all
      - answers nothing for an id the tenant does not have
      - read_contact_region_addresses
      - takes the preferred setting from the one address row that holds the preferred flag
      - takes the home setting from the home row
      - takes the work setting from the work row
      - takes the school setting from the school row
      - 'takes the setting from the row of a type the tenant made itself, as from a baseline one'
      - answers nothing for a type the contact has no address of
      - leaves a contact with no address at all out of the map rather than answering an empty address for it
      - answers an empty map when it is given no ids at all
      - which contacts link an organization’s records (integration)
      - list_contacts_linking_organization_records
      - >-
        answers the contacts linking an address, in id order, and not the one holding an equal address of its
        own
      - answers the contacts linking a phone number
      - 'answers each contact once, however many of the records it links'
      - 'answers nothing, asking the database nothing, for no record at all'
      - 'answers nothing in another tenant, though the record ids are the same strings'
      - the contact reads a write takes a lock with (integration)
      - lock_contact
      - >-
        answers the contact’s row as the TABLE holds it: every column, the computed names, and the instants as
        Postgres sends them
      - 'answers the lock the row carries, which is what the update and the delete refuse on'
      - 'refuses an id the tenant has no contact for, by RecordNotFound'
      - 'refuses a contact only another tenant has, by RecordNotFound'
      - 'answers the other tenant its own row for the id both tenants hold, never the first tenant’s'
      - lock_organization_members
      - >-
        answers the ids of every contact with an affiliation to the organization effective today, whatever its
        status, primary or not
      - 'answers at most the limit it is given, every id it answers one of the organization’s own'
      - leaves out a contact whose affiliation to the organization has ended
      - answers no id at all for an organization of the tenant that has no contact
      - 'answers no id at all for an organization the tenant does not have, rather than refusing the save'
      - answers the other tenant only its own member for the shared organization id
      - locked_element_of
      - answers nothing while no element of the contact is locked
      - names the locked e-mail address by its collection’s label and its id
      - names the locked phone number by its collection’s label and its id
      - names the locked address by its collection’s label and its id
      - names the locked communication preference by its collection’s label and its id
      - names the locked affiliation by its label and its id
      - names the locked affiliation role by its label and its id
      - names the affiliation before its role when both are locked
      - names the first collection of the record’s order when two collections hold a locked element
      - lock_contact_ids_after
      - 'answers the tenant’s next contact ids after the cursor in byte order, at most the limit'
      - 'begins at the contact after the cursor, never at the contact the cursor names'
      - answers nothing for a cursor past every contact id of the tenant
      - 'never answers a contact of another tenant, though its id follows every one of this tenant’s'
      - the contact versions a data job reads before it writes (integration)
      - answers the current version of every id it was given
      - 'leaves out an id the tenant does not have, which is how a job rejects it as not found'
      - >-
        leaves out a contact the submitter may not reach, so a job refuses it as not found rather than as
        forbidden
      - >-
        answers every contact but the ones a denial named, for a submitter whose set is everything except
        those
      - 'answers nothing at all for a submitter whose access set is empty, rather than everything'
      - 'answers an empty map when it is given no ids, without reaching the database'
      - never answers another tenant's contact for an id only that tenant has
      - 'what a contact’s delete would reach, as a data job reports it (integration)'
      - 'counts every row the cascade would take from a contact that has one of each, and nothing it would not'
      - 'answers zero for every name of a contact that holds nothing, rather than leaving the names out'
      - counts the spouse references the delete would clear on OTHER contacts
      - 'reports a locked contact as locked, which is the refusal the delete would make'
      - 'reports a contact with a locked element as locked too, and still counts what it holds'
      - >-
        reports a contact with a locked affiliation as locked, which is the refusal the delete would make for
        it too
      - 'leaves out an id the tenant does not have, which is how a job rejects it as not found'
      - 'leaves out a contact the submitter may not reach, so a job tells them nothing about it'
      - 'answers an empty map when it is given no ids, without reaching the database'
      - never answers another tenant's contact for an id only that tenant has
      - 'getContactsByIds, the read of one contact made over a set (integration)'
      - >-
        answers the tenant’s contacts with those ids in id order, leaving out an id nobody has and another
        tenant’s
      - answers each contact exactly as the read of that one contact does
      - answers no contacts for no ids
      - the contacts list narrowed to the contacts a picker may offer (integration)
      - >-
        answers a contact of every status when the list is not narrowed, so the cases below are about the
        filter
      - >-
        leaves out the inactive and the deceased contacts, and keeps the active and the duplicate ones, in
        number order
      - narrows nothing when selectable is false
      - 'narrows the contact capability’s page the same way, with each contact’s collections'
      - 'pages the narrowed list by the contact number, so a page of two continues where it ended'
      - 'answers a name search and the filter together: a match that is selectable'
      - 'answers a name search and the filter together: a match that is not selectable is left out'
    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/integration/services/persistence/contacts.seasonal-addresses.integration.spec.js
    declared_test_titles:
      - seasonal addresses (integration)
      - stores a season that crosses the new year and reads it back as month/day values
      - stores a Feb 29 end as given
      - 'stores the contact’s mark that the season is preferred, and the regular preferred address beside it'
      - mails to the seasonal address in season when the contact marks it
      - mails to the regular preferred address out of season
      - mails to the regular preferred address in season when the contact has not marked the season
      - changes a season on update and reads the new one back
    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/integration/services/persistence/contacts.spouse.integration.spec.js'
    declared_test_titles:
      - reciprocal spouse links (integration)
      - linking
      - links the other spouse back when a contact is created naming it
      - links the other spouse back when an update names it
      - bumps the other spouse's version once
      - records the link in the other spouse's own history
      - 'refuses to link a contact already recorded as the spouse of a third contact, and writes neither side'
      - 'refuses to link a contact locked against updates, and writes neither side'
      - unlinking
      - unlinks the other spouse when one side clears the link
      - unlinks the contact that created the link when the other side clears it
      - unlinks the old spouse and links the new one when a contact relinks
      - clears the other spouse when one is deleted
      - a save that leaves the spouse as it was
      - 'writes nothing to the other spouse, version included'
    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/integration/services/persistence/endpoint-deletions.integration.spec.js
    declared_test_titles:
      - the owner delete rule inside a contact and an organization delete (integration)
      - 'a contact, switch on'
      - deletes the contact
      - 'calls the function once, with the execution tenant, crm.contacts and the contact id'
      - 'a contact whose function reports a cascade, switch on'
      - deletes the contact and has called the function
      - 'a contact an open association restricts, switch on'
      - refuses with IllegalOperation (409) carrying the function message
      - deletes nothing
      - 'a contact whose function raises an error that is not a refusal, switch on'
      - 'lets the error surface as it is, not as a 409, and deletes nothing'
      - 'an organization, switch on'
      - deletes the organization
      - 'calls the function once, with the execution tenant, crm.organizations and the organization id'
      - 'an organization an open association restricts, switch on'
      - refuses with IllegalOperation (409) carrying the function message
      - deletes nothing
      - the switch off
      - 'never calls the function and deletes a contact the function would refuse, when it is unset'
      - 'never calls the function and deletes a contact the function would refuse, when it is off'
      - 'never calls the function and deletes a contact the function would refuse, when it is misspelled'
      - never calls the function and deletes an organization the function would refuse
    skipped_declarations: []
    dependency_summary: >-
      The prepared PostgreSQL database and its declared constraints, policies and transaction behavior are
      real dependencies.
command: >-
  npx mocha --timeout 60000
  'modules/entities/tests/integration/db/contact-addresses.table.integration.spec.js'
  'modules/entities/tests/integration/db/contact-communication-preferences.table.integration.spec.js'
  'modules/entities/tests/integration/db/contact-custom-field-values.table.integration.spec.js'
  'modules/entities/tests/integration/db/contact-email-addresses.table.integration.spec.js'
  'modules/entities/tests/integration/db/contact-phone-numbers.table.integration.spec.js'
  'modules/entities/tests/integration/db/contact-roles.table.integration.spec.js'
  'modules/entities/tests/integration/db/contacts.table.integration.spec.js'
  'modules/entities/tests/integration/services/contacts/contact-create.integration.spec.js'
  'modules/entities/tests/integration/services/persistence/contact-roles.integration.spec.js'
  'modules/entities/tests/integration/services/persistence/contact-roles.queries.integration.spec.js'
  'modules/entities/tests/integration/services/persistence/contacts-record.queries.integration.spec.js'
  'modules/entities/tests/integration/services/persistence/contacts.archive.integration.spec.js'
  'modules/entities/tests/integration/services/persistence/contacts.conflicts.integration.spec.js'
  'modules/entities/tests/integration/services/persistence/contacts.field-rules.integration.spec.js'
  'modules/entities/tests/integration/services/persistence/contacts.integration.spec.js'
  'modules/entities/tests/integration/services/persistence/contacts.queries.integration.spec.js'
  'modules/entities/tests/integration/services/persistence/contacts.seasonal-addresses.integration.spec.js'
  'modules/entities/tests/integration/services/persistence/contacts.spouse.integration.spec.js'
  'modules/entities/tests/integration/services/persistence/endpoint-deletions.integration.spec.js'
retries: none
proves_controls: []
product_intent: proposed
implementation_evidence: observed
narrative:
  status: enriched
---

# Contact aggregate persistence

## Verification Boundary

The contact service and its owned tables cross the PostgreSQL transaction boundary, including contact points, custom values, preferences, spouse links and endpoint deletion.

## What This Suite Proves

Fresh reads establish contact creation and correction, field limits, canonical communication values, seasonal dates and reciprocal spouse effects. Table writers test named uniqueness and foreign-key refusals, including workspace references. Contact creation tests add the aggregate service above row writes; deletion tests distinguish owned endpoint data from information that survives for another owner.

## What It Does Not Prove

Organization hierarchy behavior belongs to organization integration. Affiliation timing and borrowed details belong to affiliations integration; historical capture belongs to locks and revisions. Direct contact service entry does not prove route permissions, which the local API lifecycle suite exercises.

## Level and Rationale

Constraints and reciprocal changes can fail only when real rows interact. Pure planners cannot establish the resulting spouse records or the rollback of a failed aggregate write. The database tier also reaches malformed values that the public schema refuses earlier.

## Real Dependencies and Test Doubles

PostgreSQL, execution contexts and the contact persistence services are real. Contact photo cases install the in-memory storage pipeline; those cases establish reference adoption and compensation rather than an AWS copy. Platform-owned endpoint fixtures provide the information on which deletion decisions operate.

## Must Not Be Doubled

The contact row, its owned collections, the spouse counterpart and the transaction must stay real. Read-back assertions must use persisted records rather than reuse the save answer; replacing any of these would conceal asymmetric links or a partially committed correction.

## Fixtures and Isolation

Specs create their own contacts, counterpart records and collection rows with the shared test identifiers. Their stages provide a valid premise before each refusal. Cleanup removes owned rows and restores injected pipeline dependencies; run this cohesive group serially because process-wide dependency seams are changed by selected specs.

## Determinism and Retries

Seasonal fixtures state month/day boundaries explicitly and spouse fixtures use stable test identifiers. Stages run once and propagate setup failure to dependent assertions. No retry is added to repair failed writes or hide a uniqueness race; any nondeterministic failure needs a separately diagnosed finding.

## How It Runs

Run the command in the frontmatter from the service repository after preparing the test database. It selects every file in this aggregate register under the repository Mocha bootstrap and runs serially. This is the aggregate selection of the integration gate; the complete repository integration command includes other modules and is wider than this register.

## Coverage Gaps

This group exercises persistence without public dispatch. It does not promote those writes to API lifecycle proof. The existing detailed coverage findings for contact boundaries, preference transitions and competing collection edits remain applicable; no new technical obligation is claimed before components are authored.
