---
id: TST-CRM-ENT-790
title: Affiliations and dated roles integration
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-information-effective.view.integration.spec.js
  - modules/entities/tests/integration/db/contact-information-sources.table.integration.spec.js
  - >-
    modules/entities/tests/integration/db/contact-organization-affiliations-for-queries.view.integration.spec.js
  - modules/entities/tests/integration/db/contact-organization-affiliations.table.integration.spec.js
  - modules/entities/tests/integration/db/contact-organization-roles.table.integration.spec.js
  - >-
    modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.follow-ups.integration.spec.js
  - >-
    modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.integration.spec.js
  - >-
    modules/entities/tests/integration/services/contacts/role-assignment/automatic-role-reconciliation.integration.spec.js
  - >-
    modules/entities/tests/integration/services/contacts/role-assignment/contact-role-assignment.integration.spec.js
  - >-
    modules/entities/tests/integration/services/persistence/contact-affiliations.business-dates.integration.spec.js
  - modules/entities/tests/integration/services/persistence/contact-affiliations.integration.spec.js
  - modules/entities/tests/integration/services/persistence/contact-affiliations.queries.integration.spec.js
  - modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js
implements:
  - VT-IMC-CRM-ENT-109-03
  - VT-IMC-CRM-ENT-163-01
  - VT-IMC-CRM-ENT-022-01
  - VT-IMC-CRM-ENT-009-01
  - ATS-CRM-ENT-011-01
  - ATS-CRM-ENT-011-02
  - ATS-CRM-ENT-006-11
  - ATS-CRM-ENT-011-09
  - ATS-CRM-ENT-010-04
  - ATS-CRM-ENT-010-05
  - ATS-CRM-ENT-010-06
  - ATS-CRM-ENT-011-06
  - ATS-CRM-ENT-011-07
  - ATS-CRM-ENT-011-08
  - ATS-CRM-ENT-011-05
  - ATS-CRM-ENT-011-11
  - ATS-CRM-ENT-010-01
  - ATS-CRM-ENT-009-06
  - ATS-CRM-ENT-011-04
  - ATS-CRM-ENT-006-01
  - ATS-CRM-ENT-006-02
  - ATS-CRM-ENT-006-03
  - ATS-CRM-ENT-006-04
  - ATS-CRM-ENT-006-05
  - ATS-CRM-ENT-006-08
  - ATS-CRM-ENT-006-09
  - ATS-CRM-ENT-006-10
  - ATS-CRM-ENT-009-14
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: >-
  Fixtures create their own people, employers, affiliation types and role periods, then remove them in
  reference order. Date follow-up tests own the watermark and pending-work premises they change. Run this
  group without another reconciliation suite changing the same seeded workspace calendar or dependency seam.
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-011-01
    source: >-
      services-crm:modules/entities/tests/integration/db/contact-organization-roles.table.integration.spec.js#commits
      the role, within its affiliation by then
  - scenario: ATS-CRM-ENT-011-02
    source: >-
      services-crm:modules/entities/tests/integration/db/contact-organization-roles.table.integration.spec.js#refuses
      a second Billing role on one affiliation whose period overlaps the first
  - scenario: ATS-CRM-ENT-006-11
    source: >-
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.follow-ups.integration.spec.js#links
      nothing at the save of an upcoming affiliation
  - scenario: ATS-CRM-ENT-006-11
    source: >-
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.follow-ups.integration.spec.js#links
      every default of its type on the start day when the save stated no choice
  - scenario: ATS-CRM-ENT-011-09
    source: >-
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.follow-ups.integration.spec.js#gives
      a primary affiliation to such an organization no automatic role the type forbids, at its save
  - scenario: ATS-CRM-ENT-011-09
    source: >-
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.follow-ups.integration.spec.js#skips
      the forbidden role on the start day
  - scenario: ATS-CRM-ENT-011-09
    source: >-
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.follow-ups.integration.spec.js#says
      so in the change list: not allowed for the type
  - scenario: ATS-CRM-ENT-010-04
    source: >-
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.integration.spec.js#takes
      the organization out of compliance the next morning, with no save of the contact
  - scenario: ATS-CRM-ENT-010-04
    source: >-
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.integration.spec.js#brings
      the organization into compliance on its start day, with no save of the contact
  - scenario: ATS-CRM-ENT-010-04
    source: >-
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.integration.spec.js#gives
      the automatic roles on the start day and never on the eve
  - scenario: ATS-CRM-ENT-010-04
    source: >-
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.integration.spec.js#names
      the organization, the contact and the reason of the change it made
  - scenario: ATS-CRM-ENT-010-05
    source: >-
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.integration.spec.js#finds
      the day settled and changes nothing, the organization’s version included
  - scenario: ATS-CRM-ENT-010-05
    source: >-
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.integration.spec.js#on
      a window left open recalculates again and still writes nothing the first run wrote
  - scenario: ATS-CRM-ENT-010-05
    source: >-
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.integration.spec.js#only
      moves forward
  - scenario: ATS-CRM-ENT-010-06
    source: >-
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.integration.spec.js#reports
      what the real run then writes, change for change
  - scenario: ATS-CRM-ENT-011-06
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/automatic-role-reconciliation.integration.spec.js#gives
      nothing on the eve of the start day, when UTC has already reached it
  - scenario: ATS-CRM-ENT-011-06
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/automatic-role-reconciliation.integration.spec.js#gives
      the automatic and first-contact roles on the start day in the tenant calendar
  - scenario: ATS-CRM-ENT-011-06
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/automatic-role-reconciliation.integration.spec.js#gives
      the roles in the save that moves the start to a day already past
  - scenario: ATS-CRM-ENT-011-06
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/automatic-role-reconciliation.integration.spec.js#gives
      the roles on the new start day when a correction moved the start later
  - scenario: ATS-CRM-ENT-011-06
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/automatic-role-reconciliation.integration.spec.js#gives
      nothing to an affiliation a correction ended before its roles were given
  - scenario: ATS-CRM-ENT-011-07
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/automatic-role-reconciliation.integration.spec.js#gives
      roles with no dates of their own, so they last exactly as long as the affiliation
  - scenario: ATS-CRM-ENT-011-07
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/automatic-role-reconciliation.integration.spec.js#ends
      the given roles with the affiliation when a correction ends it
  - scenario: ATS-CRM-ENT-011-07
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/automatic-role-reconciliation.integration.spec.js#does
      not give back a role a person removed after it was given
  - scenario: ATS-CRM-ENT-011-07
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/automatic-role-reconciliation.integration.spec.js#gives
      a contact locked against updates nothing while it is locked
  - scenario: ATS-CRM-ENT-011-07
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/automatic-role-reconciliation.integration.spec.js#changes
      nothing on a second pass, version included
  - scenario: ATS-CRM-ENT-011-08
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/automatic-role-reconciliation.integration.spec.js#gives
      a deceased contact no roles on the start day
  - scenario: ATS-CRM-ENT-011-05
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/contact-role-assignment.integration.spec.js#gives
      the organization's first contact the automatic and first-contact roles, besides the roles it states
  - scenario: ATS-CRM-ENT-011-05
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/contact-role-assignment.integration.spec.js#gives
      a later contact only the automatic roles
  - scenario: ATS-CRM-ENT-011-05
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/contact-role-assignment.integration.spec.js#gives
      the roles on an existing affiliation the save makes primary (Make Primary)
  - scenario: ATS-CRM-ENT-011-05
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/contact-role-assignment.integration.spec.js#leaves
      the roles as sent when the primary organization does not change
  - scenario: ATS-CRM-ENT-011-05
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/contact-role-assignment.integration.spec.js#stores
      an automatic role once when the contact already states it
  - scenario: ATS-CRM-ENT-011-08
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/contact-role-assignment.integration.spec.js#is
      given no roles when it is created inactive with a primary organization
  - scenario: ATS-CRM-ENT-011-08
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/contact-role-assignment.integration.spec.js#is
      given no roles when it is created deceased with a primary organization
  - scenario: ATS-CRM-ENT-011-08
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/contact-role-assignment.integration.spec.js#keeps
      the roles it already holds when it becomes inactive
  - scenario: ATS-CRM-ENT-011-08
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/contact-role-assignment.integration.spec.js#counts
      an inactive contact as already belonging to the organization, so the next contact is not its first
  - scenario: ATS-CRM-ENT-011-11
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/contact-role-assignment.integration.spec.js#gives
      the first-contact roles to exactly one of several contacts joining an empty organization at once
  - scenario: ATS-CRM-ENT-011-11
    source: >-
      services-crm:modules/entities/tests/integration/services/contacts/role-assignment/contact-role-assignment.integration.spec.js#also
      when contacts move into the empty organization at once
  - scenario: ATS-CRM-ENT-010-01
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.business-dates.integration.spec.js#reads
      a tie ending on the tenant's today as current
  - scenario: ATS-CRM-ENT-010-01
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.business-dates.integration.spec.js#reads
      a tie starting on the tenant's tomorrow, already UTC's today, as future
  - scenario: ATS-CRM-ENT-010-01
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.business-dates.integration.spec.js#lists
      under current the ties effective on the tenant today, never the upcoming one
  - scenario: ATS-CRM-ENT-010-01
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.business-dates.integration.spec.js#refuses
      a primary starting on the tenant's tomorrow, though it is already UTC's and Sydney's today
  - scenario: ATS-CRM-ENT-010-01
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.business-dates.integration.spec.js#starts
      the Organization field's primary on the tenant's today when the command names no date
  - scenario: ATS-CRM-ENT-009-06
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.integration.spec.js#clears
      its primary flag
  - scenario: ATS-CRM-ENT-009-06
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.integration.spec.js#makes
      nothing primary in its place
  - scenario: ATS-CRM-ENT-011-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.integration.spec.js#takes
      an allowed role on a new affiliation (premise)
  - scenario: ATS-CRM-ENT-011-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.integration.spec.js#refuses
      a role outside the list on a new affiliation, naming the role and the type
  - scenario: ATS-CRM-ENT-011-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.integration.spec.js#refuses
      a role outside the list added to an existing affiliation
  - scenario: ATS-CRM-ENT-011-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.integration.spec.js#refuses
      an existing role changed to one outside the list
  - scenario: ATS-CRM-ENT-011-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.integration.spec.js#does
      not check a role the save leaves as it is
  - scenario: ATS-CRM-ENT-006-01
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#refuses
      a link to a record of an organization other than the affiliation’s
  - scenario: ATS-CRM-ENT-006-02
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#refuses
      a new link through an ended affiliation, naming the path
  - scenario: ATS-CRM-ENT-006-02
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#refuses
      a new link through an upcoming affiliation, naming the path
  - scenario: ATS-CRM-ENT-006-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#refuses
      a value sent for a linked row that differs from the organization’s, naming the field
  - scenario: ATS-CRM-ENT-006-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#takes
      a value that differs from the organization’s only by spaces as the same value (A19)
  - scenario: ATS-CRM-ENT-006-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#refuses
      a value that differs from the organization’s in letter case, naming the field (A19)
  - scenario: ATS-CRM-ENT-006-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#changes
      the contact’s resolved work address
  - scenario: ATS-CRM-ENT-006-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#writes
      nothing to the contact: its version is the one before
  - scenario: ATS-CRM-ENT-006-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#stops
      resolving the value: the row reads as linked, unresolved, with no value
  - scenario: ATS-CRM-ENT-006-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#keeps
      the link through the ended affiliation: nothing is re-pointed
  - scenario: ATS-CRM-ENT-006-05
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#stamps
      the contact modified once for each of its rows that became disconnected (the trigger runs per removed
      record)
  - scenario: ATS-CRM-ENT-006-08
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#makes
      an independent copy of the resolved value when the row is saved as copied with no value of its own
  - scenario: ATS-CRM-ENT-006-08
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#keeps
      the copied value as it was copied, the organization having moved since
  - scenario: ATS-CRM-ENT-006-08
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#records
      where the copy came from and when, stamped by the save
  - scenario: ATS-CRM-ENT-006-09
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#refuses
      a save that removes the affiliation and keeps the rows linked through it
  - scenario: ATS-CRM-ENT-006-09
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#takes
      a save that removes the affiliation and, in the same save, copies one row and removes the other
  - scenario: ATS-CRM-ENT-006-10
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#links
      every default when the choice is absent
  - scenario: ATS-CRM-ENT-006-10
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#links
      nothing when the choice is an empty list
  - scenario: ATS-CRM-ENT-006-10
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#refuses
      a choice that names a mapping the type does not have
  - scenario: ATS-CRM-ENT-006-10
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#never
      overwrites a work address the contact already has
  - scenario: ATS-CRM-ENT-006-11
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#skips
      an ended affiliation on re-apply, without refusing
  - scenario: ATS-CRM-ENT-006-11
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#applies
      the type’s mappings when the command names an affiliation of it
  - scenario: ATS-CRM-ENT-009-14
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js#refuses
      a UUIDv7 written in uppercase, which is not the canonical form
evidence:
  - 'services-crm:modules/entities/tests/integration/db/contact-information-effective.view.integration.spec.js'
  - 'services-crm:modules/entities/tests/integration/db/contact-information-sources.table.integration.spec.js'
  - >-
    services-crm:modules/entities/tests/integration/db/contact-organization-affiliations-for-queries.view.integration.spec.js
  - >-
    services-crm:modules/entities/tests/integration/db/contact-organization-affiliations.table.integration.spec.js
  - 'services-crm:modules/entities/tests/integration/db/contact-organization-roles.table.integration.spec.js'
  - >-
    services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.follow-ups.integration.spec.js
  - >-
    services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.integration.spec.js
  - >-
    services-crm:modules/entities/tests/integration/services/contacts/role-assignment/automatic-role-reconciliation.integration.spec.js
  - >-
    services-crm:modules/entities/tests/integration/services/contacts/role-assignment/contact-role-assignment.integration.spec.js
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.business-dates.integration.spec.js
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.integration.spec.js
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.queries.integration.spec.js
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js
source_observations:
  - provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/db/contact-information-effective.view.integration.spec.js
    declared_test_titles:
      - contact_addresses_effective and contact_phone_numbers_effective (integration)
      - resolution
      - resolves an owned row to its own value
      - resolves a copied row to its own value
      - resolves a row linked through a current affiliation to the organization’s value
      - resolves a row linked through an ended affiliation to no value at all
      - resolves a linked phone number to the organization’s number
      - tenant isolation
      - answers another tenant none of the rows
      - as rhythm_query_reader
      - reads the rows of its tenant with their resolved values
      - reads nothing in another tenant
      - the plan of a read
      - 'reads only the contact table for the row’s own columns: the organization’s records are not joined'
      - >-
        joins the organization’s records for a read of a value, which is what makes the join removal above
        meaningful
      - reads only the contact table for a phone number’s own columns
      - the compatibility views under the old names (migration 0070)
      - answers through contact_addresses_resolved exactly the rows and columns of contact_addresses_effective
      - >-
        answers through contact_phone_numbers_resolved exactly the rows and columns of
        contact_phone_numbers_effective
      - 'lets rhythm_query_reader read every column of both old names in its tenant, as it may the new ones'
      - >-
        answers another tenant none of the rows through the old names, as the application and as the query
        reader
      - 'declares all four views security_invoker, so no layer reads as its owner'
    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-information-sources.table.integration.spec.js'
    declared_test_titles:
      - 'contact information sources: the database rules (integration)'
      - a linked row that breaks no rule
      - 'is stored, so the refusals below are the rules and not the row'
      - the composite keys
      - refuses an address of another organization than the one the row names
      - refuses an affiliation of another contact
      - refuses an affiliation to another organization than the one the row names
      - refuses a phone number of another organization than the one the row names
      - the checks
      - refuses a linked row with a value of its own
      - refuses a linked row without its source record
      - refuses a copied row without its provenance
      - refuses an owned row naming a source
      - refuses a linked phone number holding a number of its own
      - refuses a source mode that is not one of the three
      - the affiliation a linked row comes through
      - cannot be deleted while it supplies the row
      - cannot be moved to another organization
      - 'the organization record a linked row names, removed'
      - >-
        becomes a disconnected row holding the record’s last values, its source gone and its provenance
        recorded
      - 'the one-per-type unique constraint, deferrable'
      - lets one statement swap two of a contact’s addresses between two types
    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-organization-affiliations-for-queries.view.integration.spec.js
    declared_test_titles:
      - contact_organization_affiliations_for_queries (integration)
      - its columns (the guard)
      - 'has columns at all, so the comparisons below are not vacuous'
      - 'computes effective, last'
      - 'carries exactly the affiliation object’s registered columns, with tenant_id and id, and nothing else'
      - 'is declared in drizzle exactly as the database has it, column for column and in order'
      - 'effective, read as the application'
      - >-
        answers current for the affiliation with no dates, past for the one that ended, future for the one not
        started
      - >-
        answers no effective to a reader with no tenant’s today: the owner with no app.tenant_id, whom no
        policy binds
      - 'asks for the tenant’s today once per statement, as an InitPlan, never per row'
      - row-level security and the query reader
      - shows another tenant none of the rows the tenant reads
      - lets rhythm_query_reader read the tenant’s rows and their effective
    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-organization-affiliations.table.integration.spec.js
    declared_test_titles:
      - contact_organization_affiliations.table (integration)
      - a row that breaks no rule
      - 'is stored, so the refusals below are the rules and not the row'
      - refuses a second row with the same tenant and id by contact_organization_affiliations_pk
      - 'refuses a row that names no organization, naming the column'
      - refuses a lock word outside the closed list by contact_organization_affiliations_sys_locked_check
      - 'D1: at most one primary affiliation per contact (contact_organization_affiliations_one_primary_uidx)'
      - 'refuses a second primary affiliation of one contact by a direct insert (scenario 3, the database half)'
      - refuses two primary affiliations of one contact written in one statement
      - takes a primary affiliation for each of two contacts
      - takes any number of affiliations of one contact that are not primary
      - 'D2: a primary affiliation is open-ended (contact_organization_affiliations_primary_open_check)'
      - refuses a primary affiliation with an end date
      - refuses an update that gives the primary affiliation an end date
      - takes an affiliation with an end date that is not primary
      - 'D3: an affiliation ends on or after it starts (contact_organization_affiliations_dates_check)'
      - refuses an affiliation that ends before it starts
      - 'takes an affiliation of one day, ending the day it starts'
      - >-
        D4: no overlapping affiliations of one contact to one organization of one type
        (contact_organization_affiliations_no_overlap_excl)
      - refuses a second Employer affiliation to Acme whose period overlaps the first
      - 'refuses one that starts on the day the other ends, the end date being inclusive'
      - 'refuses an undated one beside a dated one, an undated period covering every day'
      - takes two Employer affiliations to Acme in separate periods (scenario 4)
      - takes an Employer and a School affiliation to Acme at once
      - takes overlapping Employer affiliations of one contact to two organizations
      - takes overlapping Employer affiliations of two contacts to one organization
      - the contact's key (contact_organization_affiliations_contact_fk)
      - refuses an affiliation of a contact the tenant does not have
      - refuses an affiliation in this tenant of another tenant's contact
      - the contact's delete
      - finds the two affiliations before the delete (premise)
      - removes its affiliations with it
      - the organization's key (contact_organization_affiliations_organization_fk)
      - refuses an affiliation to an organization the tenant does not have
      - refuses an affiliation to another tenant's organization
      - the delete of an organization a contact is affiliated with
      - 'is refused, and the organization stays'
      - 'leaves the affiliation where it was: the key cascades to nothing'
      - the affiliation type's key (contact_organization_affiliations_affiliation_type_fk)
      - refuses an affiliation of a type the tenant does not have
      - refuses an affiliation of a type only another tenant has
      - the delete of an affiliation type in use
      - 'is refused, and the type stays'
      - 'leaves the affiliation where it was: the key cascades to nothing'
    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-organization-roles.table.integration.spec.js'
    declared_test_titles:
      - tests/integration/services/persistence/contact-affiliations.integration.spec.js
      - contact_organization_roles.table (integration)
      - a row that breaks no rule
      - 'is stored, so the refusals below are the rules and not the row'
      - refuses a second row with the same tenant and id by contact_organization_roles_pk
      - refuses a lock word outside the closed list by contact_organization_roles_sys_locked_check
      - refuses a role that ends before it starts by contact_organization_roles_dates_check
      - >-
        D5: a role's dates lie within its affiliation's (contact_organization_roles_within_affiliation_check,
        deferred)
      - refuses at the commit a role that starts before its affiliation starts
      - refuses at the commit a role that ends after its affiliation ends
      - refuses at the commit a role that starts after its affiliation has ended
      - writes nothing of a transaction it refused
      - 'takes an undated role on a dated affiliation, an undated role lasting as long as the affiliation'
      - takes a role exactly as long as its affiliation
      - a role written outside its affiliation and moved within it before the commit
      - 'accepts the statement that put the role outside: the check waits for the commit'
      - 'commits the role, within its affiliation by then'
      - an affiliation shortened so that a role it holds falls outside it
      - 'is refused at the commit, and the affiliation keeps its end date'
      - >-
        D6: the same role never overlaps itself on one affiliation
        (contact_organization_roles_no_overlap_excl)
      - refuses a second Billing role on one affiliation whose period overlaps the first
      - refuses two undated Billing roles on one affiliation
      - takes the same role on one affiliation in separate periods
      - takes two different roles on one affiliation at once
      - takes the same role at once on two of a contact's affiliations
      - 'the owner key (contact_organization_roles_affiliation_fk: tenant_id, affiliation_id, contact_id)'
      - refuses a role whose contact is not its affiliation's
      - refuses a role of an affiliation the tenant does not have
      - the affiliation's delete
      - finds the two roles before the delete (premise)
      - removes its roles with it
      - the contact's delete
      - finds the role before the delete (premise)
      - removes the roles of its affiliations with it
      - the contact role a role names (contact_organization_roles_contact_role_fk)
      - refuses a contact role the tenant does not have
      - refuses a contact role only another tenant has
      - the delete of a contact role a contact holds
      - 'is refused, and the contact role stays'
      - 'leaves the role held: the key cascades to nothing'
    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/affiliations/affiliation-date-reconciliation.follow-ups.integration.spec.js
    declared_test_titles:
      - the CRM-M2 follow-ups of the date reconciliation (integration)
      - 'M2-D2: a restricted organization type and the automatic roles'
      - 'gives a primary affiliation to such an organization no automatic role the type forbids, at its save'
      - still gives the automatic role to an organization whose type does not restrict
      - skips the forbidden role on the start day
      - 'says so in the change list: not allowed for the type'
      - reports the same skip in a dry run
      - 'M2-D4: the default links of an upcoming affiliation'
      - links nothing at the save of an upcoming affiliation
      - links nothing on the eve of its start day
      - links every default of its type on the start day when the save stated no choice
      - 'honours the choice recorded at creation: only the mappings named'
      - 'links nothing for an empty choice, now or on any later night'
      - 'names the contact, the affiliation and the links it made, with the reason'
      - 'moves the version of the contact once, as a save would'
      - does nothing on a second run
      - reports the links in a dry run and writes none
      - reports in a dry run what the real run then does
      - 'skips a mapping the type has lost since the choice was made, without failing the night'
      - never puts the bookkeeping on the wire
    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/affiliations/affiliation-date-reconciliation.integration.spec.js
    declared_test_titles:
      - the overnight affiliation date reconciliation (integration)
      - a role that ends by date
      - leaves the organization in compliance on its last day
      - 'takes the organization out of compliance the next morning, with no save of the contact'
      - 'names the organization, the contact and the reason of the change it made'
      - stamps the organization with the version the recalculation made and nothing more
      - is made up by the next night when the night was skipped
      - an affiliation that starts by date
      - 'counts for nothing on the eve, when UTC has already reached its start day'
      - 'brings the organization into compliance on its start day, with no save of the contact'
      - gives the automatic roles on the start day and never on the eve
      - 'names the contact, the affiliation and the roles it gave, with the reason'
      - 'gives nothing to an inactive contact, whose due affiliation waits'
      - a second run
      - 'finds the day settled and changes nothing, the organization’s version included'
      - on a window left open recalculates again and still writes nothing the first run wrote
      - the watermark
      - 'is the day settled, in the tenant’s calendar'
      - only moves forward
      - >-
        is not moved by the settings row it is kept in: a coordinator who read the defaults before the night
        still saves them
      - a workspace that is not in use
      - 'being deleted gets no settings row from a run, which reports nothing to settle'
      - being deleted gets no settings row from the watermark write a run already under way ends with
      - 'with no settings row leaves the window one day, never the history'
      - 'active still gets its watermark moved forward, row made'
      - a dry run
      - 'reports what the real run then writes, change for change'
      - writes no role and no compliance
      - leaves the watermark alone
      - 'looks ahead to a later day when asked, and shows what that midnight would do'
      - gives nothing early by looking ahead
      - a request
      - 'refuses a real run that names a day, so no job’s role is given early'
      - refuses a dry run that looks back
      - the boundaries it reads
      - 'lists an organization once for an affiliation and its roles, with every reason and the contact'
      - 'leaves out an affiliation with no role, which changes no organization’s compliance'
      - lists an end the night after its last day and not on it
      - pages by organization id
      - the scheduled sweep
      - settles the tenant on the first pass after its midnight and finds nothing to do on the next
      - counts a tenant it cannot enter as left open and still answers
    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/role-assignment/automatic-role-reconciliation.integration.spec.js
    declared_test_titles:
      - automatic roles of future affiliations (integration)
      - the save of a future-dated affiliation
      - gives no automatic role at the save
      - reads the affiliation as future until its start day in the tenant calendar
      - never answers the due mark on the wire
      - the reconciliation
      - 'gives nothing on the eve of the start day, when UTC has already reached it'
      - gives the automatic and first-contact roles on the start day in the tenant calendar
      - 'gives roles with no dates of their own, so they last exactly as long as the affiliation'
      - bumps the contact version once when it gives roles
      - gives only the automatic roles when another contact joined the organization before the start day
      - leaves the first-contact role to the contact who joined today while the future one had not started
      - 'changes nothing on a second pass, version included'
      - does not give back a role a person removed after it was given
      - gives a contact locked against updates nothing while it is locked
      - reaches the tenant named to the sweep
      - a save on or after the start day
      - 'gives the roles of a due affiliation in the save itself, before any reconciliation'
      - a backdated correction
      - gives the roles in the save that moves the start to a day already past
      - gives nothing on the old start day when a correction moved the start later
      - gives the roles on the new start day when a correction moved the start later
      - gives nothing to an affiliation a correction ended before its roles were given
      - ends the given roles with the affiliation when a correction ends it
      - a contact that is inactive or deceased (CRM-M1 decision M1-D1)
      - gives a deceased contact no roles on the start day
      - 'does not decide about an inactive contact at all: no contact is counted for it'
      - 'leaves the affiliation due, so the contact is given its roles once it is active again'
    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/role-assignment/contact-role-assignment.integration.spec.js
    declared_test_titles:
      - contact role assignment on save (integration)
      - a create
      - >-
        gives the organization's first contact the automatic and first-contact roles, besides the roles it
        states
      - gives a later contact only the automatic roles
      - counts a contact of any status as already belonging
      - counts a contact whose current affiliation to the organization is not primary as already belonging
      - does not count a contact whose only affiliation to the organization has ended
      - does not count a contact whose only affiliation to the organization starts in the future
      - gives no roles on an affiliation that is not primary
      - stores an automatic role once when the contact already states it
      - an update
      - leaves the roles as sent when the primary organization does not change
      - 'gives the roles on the affiliation the contact joins, not on the one it leaves'
      - ends the left affiliation with the roles it held there
      - adds only the automatic roles when the organization joined already has a contact
      - gives the roles on an existing affiliation the save makes primary (Make Primary)
      - >-
        removes the primary affiliation and its roles when the contact leaves its organization for none on the
        day it joined (RB-1364)
      - one transaction with the contact
      - >-
        rolls the roles back with a create refused after the contact row was written, so the next contact is
        still the first
      - leaves the roles and the organization as they were when a move is refused (StaleObject)
      - concurrent saves
      - gives the first-contact roles to exactly one of several contacts joining an empty organization at once
      - >-
        waits for a save that holds the organization, then sees the contact it added: the lock, then a fresh
        read
      - 'does not deadlock with the linked-information copy, which reads the organization under a shared lock'
      - also when contacts move into the empty organization at once
      - tenant isolation
      - gives no roles from another tenant's configuration
      - does not count another tenant's contacts in an organization with the same id
      - a contact that is inactive or deceased (CRM-M1 decision M1-D1)
      - is given no roles when it is created inactive with a primary organization
      - is given no roles when it is created deceased with a primary organization
      - 'is given the roles of an active contact when it is created active, beside the two above'
      - is given no roles when it moves to another organization while it is deceased
      - is given no roles when the save that moves it also makes it inactive
      - keeps the roles it already holds when it becomes inactive
      - >-
        counts an inactive contact as already belonging to the organization, so the next contact is not its
        first
    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-affiliations.business-dates.integration.spec.js
    declared_test_titles:
      - affiliation dates in the tenant calendar at a time-zone boundary (integration)
      - a contact's read
      - reads a tie ending on the tenant's today as current
      - 'reads a tie starting on the tenant''s tomorrow, already UTC''s today, as future'
      - the organization page's effective filter
      - 'lists under current the ties effective on the tenant today, never the upcoming one'
      - lists under future only the tie starting on the tenant tomorrow
      - 'lists nothing under past, a tie ending on the tenant''s today not having ended'
      - the refusal of a primary that starts after today
      - 'refuses a primary starting on the tenant''s tomorrow, though it is already UTC''s and Sydney''s today'
      - 'saves a primary starting on the tenant''s today, the default a coordinator anywhere must send'
      - starts the Organization field's primary on the tenant's today when the command names no date
    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-affiliations.integration.spec.js
    declared_test_titles:
      - 'the contact aggregate: affiliations through the contact saves (integration)'
      - 'scenario 1: a contact given an organization gets a primary affiliation automatically'
      - 'answers one primary affiliation to Acme of the default type, from today, with the job title, current'
      - answers the primary organization as the contact derives it
      - takes the tenant default affiliation type from the setting when the tenant has chosen one
      - 'scenario 2: several affiliations, exactly one primary; marking another unmarks the first'
      - holds Acme as the one primary before the change (premise)
      - 'leaves exactly one primary affiliation, the one newly marked'
      - 'keeps both affiliations, the old primary as an additional one'
      - 'scenario 3: two primary affiliations are refused by the aggregate'
      - 'refuses a create naming two primaries, as ObjectInvalid naming the collection'
      - 'scenario 4: two affiliations to the same organization in different periods are both kept'
      - stores both rows
      - answers the earlier one as past and the later one as current
      - refuses an overlapping third of the same type at the same organization
      - 'scenario 12: an import saves contact, affiliations and roles in one call'
      - 'answers every affiliation and role as written, the primary carrying the contact job title'
      - reads back the same record the create answered
      - 'leaves the contact at version 1: its affiliations and roles were written with it, with no second save'
      - 'stores each affiliation once, at version 1'
      - 'stores each role once, at version 1'
      - records the create as one revision of the contact
      - 'scenario 13: concurrent primary changes keep one primary and refuse the later save'
      - lets exactly one of the two saves commit
      - 'refuses the other as stale: the record moved since it was read'
      - 'leaves at most one primary affiliation, the winner’s'
      - 'set_primary_organization: changing the organization ends the previous one by default'
      - 'ends the Acme affiliation yesterday, no longer primary, and its open role with it'
      - makes a new Globex affiliation primary from today
      - 'set_primary_organization: changing the organization with ''keep'' leaves the previous one open'
      - keeps Acme current and open-ended as an additional affiliation
      - makes Globex the primary organization
      - 'set_primary_organization: clearing the organization'
      - leaves the contact without a primary organization
      - 'keeps the affiliation as history, ended yesterday'
      - 'set_primary_organization: a future effective date'
      - 'is refused, naming the field'
      - ending the primary affiliation
      - clears its primary flag
      - makes nothing primary in its place
      - the job title kept equal to the primary affiliation’s
      - gives the primary affiliation the new title
      - 'set_primary_organization: an organization the contact already has an open affiliation to (§5.1)'
      - 'holds one affiliation to Acme, not primary, before the pick (premise)'
      - 'makes that affiliation primary, under its own id, its type and start kept'
      - a primary affiliation starting after today (§4 rule 3)
      - 'is refused, naming its start date'
      - where an affiliation stands today (§6)
      - answers an affiliation that starts after today as future
      - 'the limits (§3.4): a client that sends more is refused'
      - 'refuses 101 affiliations on one contact, naming the collection'
      - takes 100 affiliations on one contact and stores every one
      - 'takes 150 ended affiliations on one contact: only current and upcoming ones count (M2-D5)'
      - refuses 101 upcoming affiliations beside ended history
      - 'refuses 26 roles on one affiliation, naming its roles'
      - takes 25 roles on one affiliation and stores every one
      - a save that changes only an affiliation is a change of the contact (§4)
      - ends the Globex affiliation and nothing else (premise)
      - bumps the contact's version
      - records one revision of the contact naming the affiliations as what changed
      - records one change event of the contact naming the affiliations as what changed
      - ownership of affiliation and role ids
      - refuses an affiliation id that is another contact's
      - 'refuses a role id that belongs to another affiliation of the same contact, naming its path'
      - 'refuses a role id that belongs to another contact''s affiliation, naming its path'
      - an affiliation to an organization deleted while the editor was open (DEF-CRM-0739)
      - 'refuses the save on the row that names it, with its name and id, saying it may have been deleted'
      - 'refuses the Organization field naming it on the command, and leaves the contact as it was'
      - an update that omits the affiliations leaves every one of them as it was (ruling D-09)
      - 'holds a past, a current and a future affiliation with a role each before the rename (premise)'
      - changes the name it was asked to
      - 'answers every affiliation and role under its own id and version, unchanged'
      - 'keeps every affiliation and role stored, as a read answers them'
      - keeps the primary organization
      - >-
        an update that names a stored affiliation without its roles leaves its roles as they were (ruling
        D-09)
      - changes the affiliation it was asked to (premise)
      - keeps the role under its own id
      - >-
        an update that states the affiliations as an empty list removes them, an intentional operation (ruling
        D-09)
      - leaves the contact with no affiliation
      - the contact roles an organization type allows (M2-D2)
      - takes an allowed role on a new affiliation (premise)
      - 'refuses a role outside the list on a new affiliation, naming the role and the type'
      - refuses a role outside the list added to an existing affiliation
      - refuses an existing role changed to one outside the list
      - does not check a role the save leaves as it is
      - takes any role at an organization whose type does not restrict (premise of the rule)
      - affiliations stated without ids (A20)
      - 'keeps the two affiliations, not four'
      - keeps each affiliation under its own id
      - changes what the replacement states differently
      - keeps the roles of an affiliation stated without roles
      - adds an affiliation of another start date as a new one beside the matched ones
    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-affiliations.queries.integration.spec.js
    declared_test_titles:
      - an organization's page of affiliations (integration)
      - >-
        gives every affiliation and role of the fixture a generated UUIDv7 id, which the literals below then
        name (premise)
      - >-
        answers the affiliations effective today by default, each whole with its contact and its roles, in
        contact order
      - answers the affiliations that ended before today as past
      - answers the affiliations that begin after today as future
      - >-
        answers every affiliation of the organization for all, whatever its period, and none of another
        organization
      - 'answers the affiliations effective on the day as_of names, each still saying where it stands today'
      - narrows the page to one affiliation type
      - narrows the current page to the affiliations holding the role today
      - answers no current affiliation for a role held only in the past
      - narrows any other page to the affiliations holding the role on any date
      - 'answers at most the limit, and the (contact, affiliation) it ended at to come back with'
      - 'answers the next page from where the previous one ended, and no cursor on the last page'
      - answers an empty page for an organization the tenant does not have
      - answers the other tenant only its own affiliation under the same organization id
      - organizations_of_affiliations
      - 'answers the organization each affiliation names, by affiliation id'
      - leaves out an id the tenant has no affiliation for
      - never answers another tenant's affiliation
      - answers an empty map when it is given no id at all
      - the compliance rule's view of an organization's contacts (integration)
      - 'answers one row per contact in id order, the e-mail address taken from slot 1 and no other slot'
      - 'answers one row for a contact that has three e-mail addresses, never one row per address'
      - answers an empty list for an organization the tenant does not have
    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-information-sources.integration.spec.js
    declared_test_titles:
      - reference-based contact information through the contact aggregate (integration)
      - 'linked work and school addresses through two affiliations, and phone numbers from two organizations'
      - resolves the work address to the employer’s and the school address to the school’s
      - resolves the work and the school phone numbers from the two organizations
      - names the affiliation and the organization each linked address comes through
      - 'stores no value on a linked row: the value is the organization’s'
      - an organization address that changes
      - changes the contact’s resolved work address
      - 'writes nothing to the contact: its version is the one before'
      - copied and owned values
      - 'keeps the copied value as it was copied, the organization having moved since'
      - 'records where the copy came from and when, stamped by the save'
      - keeps the owned value as the contact gave it
      - an affiliation that ends
      - 'keeps the link through the ended affiliation: nothing is re-pointed'
      - 'stops resolving the value: the row reads as linked, unresolved, with no value'
      - 'leaves the work address, linked through the current affiliation, resolving'
      - the organization’s record removed
      - 'turns the linked work address into a disconnected row holding its last value, not a copy (D-25)'
      - turns the linked work number into a disconnected row holding its last number
      - 'records the organization the value came from, why and when, and no source any more'
      - 'records the same of the number, with the reason of a removed phone number'
      - 'says on the wire that each needs verification, so no value is presented as synced'
      - says nothing of verification on a row that is linked
      - >-
        stamps the contact modified once for each of its rows that became disconnected (the trigger runs per
        removed record)
      - >-
        refuses a replace of the contact at the version read before the removal as StaleObject, not for the
        link it still names (DEF-CRM-0112)
      - an affiliation that still supplies a row
      - refuses a save that removes the affiliation and keeps the rows linked through it
      - 'takes a save that removes the affiliation and, in the same save, copies one row and removes the other'
      - changing a linked row
      - 'refuses a value sent for a linked row that differs from the organization’s, naming the field'
      - takes a value that differs from the organization’s only by spaces as the same value (A19)
      - 'refuses a value that differs from the organization’s in letter case, naming the field (A19)'
      - 'refuses copy provenance sent on a linked address, naming the field (DEF-CRM-0017)'
      - 'refuses copy provenance sent on a linked phone number, naming the field (DEF-CRM-0017)'
      - 'refuses a copy stamp sent on a linked address, naming the field'
      - 'takes a linked row sent back exactly as it was read, which answers no provenance'
      - re-points a row to another affiliation’s organization record ("Change source")
      - refuses a link to a record of an organization other than the affiliation’s
      - >-
        makes an independent copy of the resolved value when the row is saved as copied with no value of its
        own
      - ids across ordinary saves (§0b.2 item 4)
      - keeps the organization address’s id when the organization is saved with another address added
      - keeps every affiliation’s id when the contact is saved
      - keeps every address’s and phone number’s id when the contact is saved
      - leaves the links resolving after both saves
      - an affiliation type’s default mappings (§0b.2 item 6)
      - links the work address and number of a contact created with an affiliation of the type
      - never overwrites a work address the contact already has
      - 'links nothing for an affiliation of a type that has no mappings, the command included'
      - applies the type’s mappings when the command names an affiliation of it
      - refuses the command for an affiliation that is not the contact’s
      - the default links a coordinator kept on a new affiliation (M2-D1)
      - links every default when the choice is absent
      - links nothing when the choice is an empty list
      - links only the mappings named
      - does not answer the choice on the affiliation
      - refuses a choice that names a mapping the type does not have
      - 'does not apply a choice to a re-apply: the command links every default'
      - 'affiliations not in effect today supply no default links (M2-D4, A22)'
      - links nothing through an ended or an upcoming affiliation on create
      - 'skips an ended affiliation on re-apply, without refusing'
      - 'refuses a new link through an ended affiliation, naming the path'
      - 'refuses a new link through an upcoming affiliation, naming the path'
      - an affiliation type’s default mappings for a current and an ended affiliation of the type
      - created the third contact with the ended affiliation’s id sorting before the current one’s
      - links the work address to the current employer when the current affiliation is listed first
      - links the work address to the current employer when the ended affiliation is listed first
      - links the work address to the current employer when the ended affiliation’s id sorts first
      - 'a new affiliation whose id its client named, and the rows linked through it in the same save'
      - creates the affiliation under the id its client named
      - 'links the work address and number of the create through the new affiliation, resolved to Acme’s'
      - names the new affiliation and its organization as the source of both rows of the create
      - 'adds the College affiliation of the update under the id its client named, keeping Acme’s'
      - 'links the school address of the update through the new College affiliation, resolved to the College’s'
      - a client-named id a new affiliation may not take
      - 'holds the affiliation whose id is reused, so the refusal below is about that id (premise)'
      - 'refuses an id another affiliation of the tenant already has, naming it'
      - 'refuses an id that is not a UUIDv7, naming it'
      - refuses a UUID of another version (a random UUIDv4)
      - 'refuses a UUIDv7 written in uppercase, which is not the canonical form'
      - refuses the same new id named for two affiliations of one save
      - 'a linked row naming its organization’s record by type, through an affiliation created in the same save'
      - 'stores the link to Acme’s main address and main number, the ids the types resolved to'
      - 'resolves both rows to Acme’s values, through the new affiliation'
      - 'answers the record ids and never the write-only type properties, in the create’s answer and in a read'
      - 'a linked row naming its organization’s record by type, through an existing affiliation'
      - 'links the work number to Acme’s fax, through the existing affiliation'
      - 'keeps the row, its id and its link when saved again with the type and the id it resolves to'
      - 'keeps the row, its id and its link when saved again with the type alone'
      - a type that does not name one record of the affiliation’s organization
      - holds Acme with a main address and none of the billing type (premise)
      - 'refuses a type the organization has no record of, naming the item, the organization and the type'
      - 'refuses a type and an id that name different records, naming the item'
      - 'refuses a type of the other kind (a phone number type named as an address type), naming the item'
      - 'refuses a type on a row that is not linked, rather than ignoring it'
      - leaves the contact with no address after the refused saves
      - a type resolved in another tenant
      - holds a main address of the suite tenant’s organization under the same id (premise)
      - 'resolves the type against the other tenant’s organization only, which has no main address'
      - two saves of one contact at the same version
      - lets exactly one of them commit
      - tells the other the record moved on
      - another tenant
      - cannot read the linked contact
      - cannot link its own contact to this tenant’s organization record
    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-information-effective.view.integration.spec.js'
  'modules/entities/tests/integration/db/contact-information-sources.table.integration.spec.js'
  'modules/entities/tests/integration/db/contact-organization-affiliations-for-queries.view.integration.spec.js'
  'modules/entities/tests/integration/db/contact-organization-affiliations.table.integration.spec.js'
  'modules/entities/tests/integration/db/contact-organization-roles.table.integration.spec.js'
  'modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.follow-ups.integration.spec.js'
  'modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.integration.spec.js'
  'modules/entities/tests/integration/services/contacts/role-assignment/automatic-role-reconciliation.integration.spec.js'
  'modules/entities/tests/integration/services/contacts/role-assignment/contact-role-assignment.integration.spec.js'
  'modules/entities/tests/integration/services/persistence/contact-affiliations.business-dates.integration.spec.js'
  'modules/entities/tests/integration/services/persistence/contact-affiliations.integration.spec.js'
  'modules/entities/tests/integration/services/persistence/contact-affiliations.queries.integration.spec.js'
  'modules/entities/tests/integration/services/persistence/contact-information-sources.integration.spec.js'
retries: none
proves_controls: []
product_intent: proposed
implementation_evidence: observed
narrative:
  status: enriched
---

# Affiliations and dated roles integration

## Verification Boundary

Contact aggregate relationship writes, organization affiliation reads, date reconciliation and role assignment cross real relational and transaction boundaries.

## What This Suite Proves

The tests establish affiliation identity and dated role periods, primary selection, effective current/past reads, organization-derived contact details and link removal. Automatic-role tests compare assignments with stored results. Reconciliation tests cover business-day boundaries and deferred follow-up handling, including failures that keep later work pending.

## What It Does Not Prove

Classification catalogue CRUD belongs to the types and settings group. These service tests do not exercise the schedule trigger or deployed queue delivery. A successful computed relationship plan alone is insufficient evidence for a browser lifecycle or the permission matrix at the public dispatcher.

## Level and Rationale

Overlapping periods, source references and automatic-role writes join several tables. Real database state is necessary to establish identity preservation, atomic relationship replacement and the behavior of deferred work after a day changes. Isolated date arithmetic does not cross those seams.

## Real Dependencies and Test Doubles

PostgreSQL, the affiliation persistence functions, role assignment and reconciliation services are real. Test execution contexts and explicit business-date fixtures provide the workspace calendar. Declared follow-up seams isolate failures; they do not represent successful remote settlement unless the source asserts the persisted effect.

## Must Not Be Doubled

Never replace affiliation and role tables, effective information views or the transactions under test. Keep the actual time-zone conversion for date-boundary cases. A supplied organization page or role list would bypass the graph and could hide a relationship assigned to the wrong workspace.

## Fixtures and Isolation

Fixtures create their own people, employers, affiliation types and role periods, then remove them in reference order. Date follow-up tests own the watermark and pending-work premises they change. Run this group without another reconciliation suite changing the same seeded workspace calendar or dependency seam.

## Determinism and Retries

The business-date fixtures include time-zone boundaries and explicit day windows. UUID values are compared through fixture variables where generated. Stages preserve the original refusal and cleanup evidence; automatic retries would blur whether pending work settled once or was applied repeatedly.

## 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

The integration tier proves local reconciliation effects and recovery, while the public date endpoint only adds dispatch and authority checks. It cannot establish that production enables the schedule. The existing timing, authority and overlapping-period findings remain the statement of missing coverage.
