---
id: TST-CRM-ENT-789
title: Organization 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/organization-addresses.table.integration.spec.js
  - modules/entities/tests/integration/db/organization-custom-field-values.table.integration.spec.js
  - modules/entities/tests/integration/db/organization-phone-numbers.table.integration.spec.js
  - modules/entities/tests/integration/db/organizations-for-queries.view.integration.spec.js
  - modules/entities/tests/integration/db/organizations.table.integration.spec.js
  - modules/entities/tests/integration/services/persistence/organizations.archive.integration.spec.js
  - modules/entities/tests/integration/services/persistence/organizations.field-rules.integration.spec.js
  - modules/entities/tests/integration/services/persistence/organizations.integration.spec.js
  - modules/entities/tests/integration/services/persistence/organizations.queries.integration.spec.js
implements:
  - ATS-CRM-ENT-002-06
  - ATS-CRM-ENT-002-08
  - ATS-CRM-ENT-013-04
  - ATS-CRM-ENT-013-06
  - ATS-CRM-ENT-013-08
  - ATS-CRM-ENT-013-09
  - ATS-CRM-ENT-002-01
  - ATS-CRM-ENT-004-06
  - ATS-CRM-ENT-005-06
  - ATS-CRM-ENT-002-03
  - ATS-CRM-ENT-002-05
  - ATS-CRM-ENT-002-07
  - ATS-CRM-ENT-002-10
  - ATS-CRM-ENT-004-08
  - ATS-CRM-ENT-005-04
  - ATS-CRM-ENT-005-05
  - ATS-CRM-ENT-014-04
  - VT-IMC-CRM-ENT-131-01
  - VT-IMC-CRM-ENT-133-01
environment: local
implementation_state: implemented
latest_result: not-run
latest_run_at: null
latest_run_ref: null
conformance: not-verified
estimated_minutes: null
estimate_basis: No measured duration is available for this cohesive group; per-file allowances are not runtime estimates.
isolation: >-
  The group creates named organizations and any required parent records. Table tests start from acceptable
  rows before violating a particular constraint. Cleanup follows dependency order so children and detail rows
  do not prevent parent cleanup; fixtures do not authorize changes to unrelated shared records.
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-002-06
    source: >-
      services-crm:modules/entities/tests/integration/db/organizations.table.integration.spec.js#refuses
      putting the top under its grandchild by organizations_parent_not_descendant
  - scenario: ATS-CRM-ENT-002-06
    source: >-
      services-crm:modules/entities/tests/integration/db/organizations.table.integration.spec.js#refuses the
      second move by organizations_parent_not_descendant once the first commits
  - scenario: ATS-CRM-ENT-002-08
    source: >-
      services-crm:modules/entities/tests/integration/db/organizations.table.integration.spec.js#refuses a
      status outside the closed list by organizations_status_check
  - scenario: ATS-CRM-ENT-013-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.archive.integration.spec.js#clears
      the three archive columns
  - scenario: ATS-CRM-ENT-013-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.archive.integration.spec.js#returns
      the organization as it was, apart from its version and modified stamps
  - scenario: ATS-CRM-ENT-013-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.archive.integration.spec.js#keeps
      the trimmed reason as unarchive_reason
  - scenario: ATS-CRM-ENT-013-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.archive.integration.spec.js#is
      refused as ObjectInvalid over 500 characters, naming the reason
  - scenario: ATS-CRM-ENT-013-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.archive.integration.spec.js#is
      cleared by the next archive
  - scenario: ATS-CRM-ENT-013-06
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.archive.integration.spec.js#is
      left out of the list by default
  - scenario: ATS-CRM-ENT-013-08
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.archive.integration.spec.js#keeps
      the ended affiliation naming the organization's id, with its name and its archive instant
  - scenario: ATS-CRM-ENT-013-08
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.archive.integration.spec.js#lists
      the former employee under Past
  - scenario: ATS-CRM-ENT-013-08
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.archive.integration.spec.js#lists
      both under All, each marked with the organization it names being archived
  - scenario: ATS-CRM-ENT-013-08
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.archive.integration.spec.js#stays
      on the organization’s people list, marked as archived
  - scenario: ATS-CRM-ENT-013-09
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.archive.integration.spec.js#is
      kept through a plain save, which never writes it
  - scenario: ATS-CRM-ENT-002-01
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.field-rules.integration.spec.js#refuses
      a name of 201 characters, naming name
  - scenario: ATS-CRM-ENT-002-01
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.field-rules.integration.spec.js#refuses
      notes of 10,001 characters, naming notes
  - scenario: ATS-CRM-ENT-004-06
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.field-rules.integration.spec.js#refuses
      a replacement whose number differs from the stored one, naming number
  - scenario: ATS-CRM-ENT-005-06
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.field-rules.integration.spec.js#refuses
      an unknown country on an address, naming the address’s country
  - scenario: ATS-CRM-ENT-002-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.integration.spec.js#answers
      the first number, 10000, and leaves 10001 in the seed
  - scenario: ATS-CRM-ENT-002-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.integration.spec.js#saves
      the organization under the highest number plus one, 4001
  - scenario: ATS-CRM-ENT-002-03
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.integration.spec.js#renumbers
      after the tenant's highest organization number, never its contacts'
  - scenario: ATS-CRM-ENT-002-05
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.integration.spec.js#refuses
      a parent that is an organization of another tenant, by organizations_parent_fk
  - scenario: ATS-CRM-ENT-002-06
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.integration.spec.js#names
      an organization that is its own parent as a field problem, before the database sees it
  - scenario: ATS-CRM-ENT-002-07
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.integration.spec.js#answers
      the chapter its ancestors nearest first, their number and the top level
  - scenario: ATS-CRM-ENT-002-07
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.integration.spec.js#answers
      the descendant, never saved again, its ancestry under the new parent
  - scenario: ATS-CRM-ENT-002-07
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.integration.spec.js#answers
      the computed ancestry on create, not what was sent
  - scenario: ATS-CRM-ENT-002-08
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.integration.spec.js#defaults
      the status to active
  - scenario: ATS-CRM-ENT-002-10
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.integration.spec.js#refuses
      the update as ObjectInvalid, naming the logo field
  - scenario: ATS-CRM-ENT-002-10
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.integration.spec.js#leaves
      the organization holding the logo it had
  - scenario: ATS-CRM-ENT-004-08
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.integration.spec.js#refuses
      the create as ObjectInvalid, listing both problems
  - scenario: ATS-CRM-ENT-005-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.integration.spec.js#formats
      a US number of any type, now that no type is the legacy `other_phone_numbers`
  - scenario: ATS-CRM-ENT-005-05
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.integration.spec.js#promotes
      the next address of the tenant’s display order to preferred
  - scenario: ATS-CRM-ENT-014-04
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organizations.integration.spec.js#refuses
      the delete as IllegalOperation, naming the element that is locked
evidence:
  - 'services-crm:modules/entities/tests/integration/db/organization-addresses.table.integration.spec.js'
  - >-
    services-crm:modules/entities/tests/integration/db/organization-custom-field-values.table.integration.spec.js
  - 'services-crm:modules/entities/tests/integration/db/organization-phone-numbers.table.integration.spec.js'
  - 'services-crm:modules/entities/tests/integration/db/organizations-for-queries.view.integration.spec.js'
  - 'services-crm:modules/entities/tests/integration/db/organizations.table.integration.spec.js'
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/organizations.archive.integration.spec.js
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/organizations.field-rules.integration.spec.js
  - 'services-crm:modules/entities/tests/integration/services/persistence/organizations.integration.spec.js'
  - >-
    services-crm:modules/entities/tests/integration/services/persistence/organizations.queries.integration.spec.js
source_observations:
  - provenance: test
    source: 'services-crm:modules/entities/tests/integration/db/organization-addresses.table.integration.spec.js'
    declared_test_titles:
      - organization_addresses.table (integration)
      - 'the database constraints of crm.organization_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 organization_addresses_pk
      - refuses a row whose organization the tenant does not have by organization_addresses_organization_fk
      - >-
        refuses a row that names this tenant’s organization under another tenant by
        organization_addresses_organization_fk
      - refuses an address type the tenant does not have by organization_addresses_address_type_fk
      - 'accepts an address type of the tenant’s that is marked for contacts, because applies_to is a guide'
      - refuses a latitude off the globe by organization_addresses_latitude_check
      - refuses a longitude off the globe by organization_addresses_longitude_check
      - refuses a lock word outside the closed list by organization_addresses_sys_locked_check
      - refuses a second address of the same type by organization_addresses_one_per_type_uq
      - refuses a second preferred address by organization_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/organization-custom-field-values.table.integration.spec.js
    declared_test_titles:
      - organization_custom_field_values.table (integration)
      - a value in exactly one of the typed columns
      - held no value for the organization before the write
      - writes one row for the organization 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 organization and field by organization_custom_field_values_pk
      - 'accepts the same field on another organization, the key being per organization'
      - 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 organization_custom_field_values_one_value_check
      - refuses a row with two values by organization_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
        organization_custom_field_values_list_check
      - the rule that a value names a custom field key
      - refuses a key without the custom suffix by organization_custom_field_values_custom_field_id_check
      - refuses a key with a service prefix by organization_custom_field_values_custom_field_id_check
      - the organization a value belongs to
      - >-
        refuses a value for an organization the tenant does not have by
        organization_custom_field_values_organization_fk
      - >-
        refuses a value in another tenant naming this tenant's organization by
        organization_custom_field_values_organization_fk
      - deleting the organization
      - held the value while the organization was there
      - >-
        takes the values with the organization, by the cascade on
        organization_custom_field_values_organization_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/organization-phone-numbers.table.integration.spec.js'
    declared_test_titles:
      - organization_phone_numbers.table (integration)
      - 'the database constraints of crm.organization_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 organization_phone_numbers_pk
      - >-
        refuses a row whose organization the tenant does not have by
        organization_phone_numbers_organization_fk
      - >-
        refuses a row that names this tenant’s organization under another tenant by
        organization_phone_numbers_organization_fk
      - >-
        refuses a phone number type the tenant does not have by
        organization_phone_numbers_phone_number_type_fk
      - 'accepts a phone number type of the tenant’s that is marked for contacts, because applies_to is a guide'
      - refuses a lock word outside the closed list by organization_phone_numbers_sys_locked_check
      - refuses a second number of the same type by organization_phone_numbers_one_per_type_uq
      - 'refuses a row with no number, naming the column'
      - '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/organizations-for-queries.view.integration.spec.js'
    declared_test_titles:
      - organizations_for_queries (integration)
      - its columns (the guard)
      - 'has columns at all, so the comparisons below are not vacuous'
      - 'computes the two columns no column of crm.organizations holds, last and in order'
      - 'carries exactly the organization 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'
      - 'its counts, read as the application role'
      - 'shows the fixture through the view at all, so what follows reads real rows'
      - counts no contact as 0 and never as null
      - >-
        counts every contact affiliated with the organization today, and a contact with two current
        affiliations to it exactly once
      - leaves out a contact whose only affiliation to the organization has ended
      - leaves another organization’s contacts out of the count
      - 'counts direct children only: a grandchild belongs to its own parent'
      - 'stops counting a contact that is deleted: CRM deletes for real'
      - row-level security through the view
      - 'shows the owner, whom no policy binds, both tenants’ organizations under the same id'
      - 'shows each tenant only its own organization of that id, with its own count'
      - leaves the other tenant’s organizations the first tenant does not have out altogether
      - shows nothing at all to a reader that set no tenant
      - 'as rhythm_query_reader, the role the view exists for'
      - reads the tenant’s organizations and their counts through the view
      - 'reads the other tenant’s row under the same id when it is that tenant, and never both at once'
      - >-
        is refused a column of crm.organizations the object does not publish, which is why the view carries
        none
      - what a query that asks for one column reads
      - never touches crm.contact_organization_affiliations when the query selects no count
      - >-
        does touch crm.contact_organization_affiliations when the query selects the contact count, so the case
        above is not vacuous
    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/organizations.table.integration.spec.js'
    declared_test_titles:
      - organizations.table (integration)
      - 'the database constraints of crm.organizations, 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 organizations_pk
      - refuses a second row with the same tenant and organization number by organizations_number_uq
      - refuses a parent the tenant does not have by organizations_parent_fk
      - refuses a status outside the closed list by organizations_status_check
      - refuses an organization that is its own parent by organizations_parent_not_self_check
      - refuses an organization number that is not positive by organizations_number_check
      - refuses a lock word outside the closed list by organizations_sys_locked_check
      - 'refuses a row with no name, naming the column'
      - 'refuses a row with no organization number, naming the column'
      - >-
        the loop refusal of crm.organizations (trigger organizations_parent_not_descendant), for a writer that
        goes past the repository
      - 'accepts a parent change that closes no loop, so the refusals below are the loop and not the change'
      - refuses putting the top under its grandchild by organizations_parent_not_descendant
      - refuses putting the top under its child by organizations_parent_not_descendant
      - leaves the top without a parent after the refusals
      - two opposite moves at the same time
      - makes the second move wait for the first
      - refuses the second move by organizations_parent_not_descendant once the first commits
      - 'keeps the first move and leaves right at the top: no loop'
      - answers the refusal as ObjectInvalid naming the parent field
    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/organizations.archive.integration.spec.js
    declared_test_titles:
      - archive of an organization (integration)
      - archiving an organization with a past and a current affiliate
      - 'sets the instant, the user and the reason'
      - bumps the version once and keeps the status
      - keeps its addresses and phone numbers
      - 'keeps the ended affiliation naming the organization''s id, with its name and its archive instant'
      - lists the former employee under Past
      - lists the current employee under Current
      - 'lists both under All, each marked with the organization it names being archived'
      - an affiliate that is archived
      - 'stays on the organization’s people list, marked as archived'
      - an organization 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'
      - is refused as StaleObject when the version moved on
      - stays live after a stale version
      - unarchiving an organization
      - clears the three archive columns
      - 'returns the organization as it was, apart from its version and modified stamps'
      - is back in the default list
      - is saved again
      - 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 organization (organizations_unarchive_reason_check)
      - the table
      - refuses an archive instant with no user (organizations_archive_check)
      - refuses a reason on a live organization (organizations_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/organizations.field-rules.integration.spec.js
    declared_test_titles:
      - what an organization may hold and what a client may send back (integration)
      - field limits (M1-D3)
      - accepts a name of exactly 200 characters
      - 'refuses a name of 201 characters, naming name'
      - 'accepts notes of exactly 10,000 characters'
      - 'refuses notes of 10,001 characters, naming notes'
      - refuses a limit on update as on create
      - 'refuses an unknown country on an address, naming the address’s country'
      - accepts a country by name and stores its code
      - computed and internal fields sent back (M1-D5)
      - 'saves a replacement of the whole record read, with its number and stamps unchanged'
      - 'refuses a replacement whose number differs from the stored one, naming number'
      - 'refuses a replacement whose last modifier differs from the stored one, naming sys_last_modified_by_id'
      - writes nothing when it refuses a changed number
      - 'answers a stale replacement with the conflict, not with a refusal of a stamp the other save changed'
      - saves a patch that restates the number as stored
      - refuses a patch that changes the number
      - refuses a patch that removes the creation instant
      - the conflict of a save whose version moved on (M1-D6)
      - >-
        carries the latest change as a user, with the name the history gives them and the instant of the
        revision
      - >-
        names an organization changed before its history began by its own stamps: who last modified it and
        when
      - 'carries the latest change as the system when the system made it, and says so in the message'
      - is answered as a 409 whose body holds the code and the latest change
    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/organizations.integration.spec.js'
    declared_test_titles:
      - organizations.table (integration)
      - insert_organization
      - writes the row into the tenant of the execution
      - takes the tenant's next organization number from platform.number_seeds
      - advances the tenant organization-number seed by the one number it took
      - defaults the status to active
      - stores the string lists it was written with
      - stamps the new row with version 1 and the caller
      - 'gives an organization the client named no id a UUIDv7, never its number'
      - still takes the number from the seed for an organization it names itself
      - 'keeps the id the client named, which is no UUIDv7'
      - counts organizations apart from contacts
      - links a parent of the same tenant
      - 'refuses a parent that is an organization of another tenant, by organizations_parent_fk'
      - answers a parent of another tenant as ForeignKeyViolation through organization_refusal
      - 'refuses an id the tenant already uses, by organizations_pk'
      - answers the duplicate id as InvalidRequest through organization_refusal
      - 'names an organization that is its own parent as a field problem, before the database sees it'
      - 'refuses an organization that is its own parent at the database, by organizations_parent_not_self_check'
      - 'insert_organization: a number another row already holds'
      - a create that meets the taken number 2000 while the highest is 4000
      - 'saves the organization 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 organization that held 2000 exactly as it was
      - reads the saved organization back under 4001
      - a sparse range
      - 'renumbers after the highest number, never into a gap below it'
      - a tenant whose numbers run higher than this tenant's
      - renumbers after this tenant's highest organization number only
      - leaves the other tenant's seed where it was
      - 'the tenant''s contacts, numbered by a counter of their own'
      - 'renumbers after the tenant''s highest organization number, never its contacts'''
      - leaves the tenant's contact seed where it was
      - a tenant with no organization and no seed yet
      - 'answers the first number, 10000, and leaves 10001 in the seed'
      - 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
      - an organization 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 organization'
      - 'gives it a UUIDv7 id of its own, not the string "2000"'
      - leaves the seed after the number it saved
      - leaves the organization the client named "2000" as it was
      - a refusal that is not the number
      - 'refuses an id the client named that the tenant already uses by organizations_pk, without renumbering'
      - 'keeps the number the refused create took consumed, a gap'
      - 'refuses a parent the tenant does not have by organizations_parent_fk, taking one number and no more'
      - a number the client supplies
      - 'is replaced by the allocated number: an organization number is always the server''s'
      - get_organization_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_organization
      - answers the row of the tenant that has it
      - 'refuses the same id in a tenant that does not have it, as RecordNotFound'
      - update_organization_fields
      - stores the fields the replacement named
      - keeps the organization number the insert allocated
      - bumps the version
      - clears a field the replacement omits
      - refuses an unknown id as RecordNotFound
      - 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_organization_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
      - answers no cursor on the last page
      - delete_organization_row
      - deletes the row
      - 'refuses an organization that child organizations still name, as ForeignKeyViolation'
      - 'refuses an organization contacts are affiliated with, as ForeignKeyViolation'
      - 'refuses an organization a contact’s ended affiliation still names, as ForeignKeyViolation'
      - leaves an organization its affiliated contacts name in place
      - refuses a locked organization as IllegalOperation
      - leaves a locked organization in place after the refusal
      - createOrganization (integration)
      - 'an organization with addresses, phone numbers and a parent'
      - 'answers the organization under the id the client named, in the tenant of the execution'
      - allocates the tenant's next organization number
      - links the parent the input named
      - stamps the organization with version 1 and the caller
      - stores the string lists the input named
      - stores the addresses in the tenant’s type display order
      - 'stores the phone numbers in the tenant’s type display order, formatted'
      - 'keeps the tenant and the owner off the wire: they are the key, not the element'
      - 'writes one row per element, across the two owned tables'
      - reads back exactly what the create answered
      - an organization whose first number is already taken (a stale seed)
      - saves it under the highest organization number plus one
      - 'gives it a UUIDv7 id, which no renumbering changes'
      - 'writes the organization, its two addresses and its two phone numbers once each'
      - leaves no address or phone number of the refused attempt anywhere in the tenant
      - leaves 4002 in the seed
      - an organization with nothing but a name
      - 'gives it a UUIDv7 id when the client names none (Andrew, 2026-10-10), never its organization number'
      - 'still takes the tenant’s next organization number, which stays its own column'
      - reads the organization back under the id it was given
      - 'answers an empty array for each collection, never a missing property'
      - an organization whose addresses mark none of them preferred
      - marks exactly one of the addresses preferred
      - 'marks the first address of the tenant’s display order, whatever order the client sent them in'
      - an organization whose addresses mark one of them preferred
      - 'keeps the preferred address the client marked, rather than the display order’s first'
      - an id the tenant already uses
      - refuses the create as InvalidRequest
      - a parent the tenant does not have
      - 'refuses the create as ForeignKeyViolation, naming the parent'
      - creates nothing
      - 'leaves the organization number it took consumed, a gap'
      - an organization that breaks two persistence rules
      - 'refuses the create as ObjectInvalid, listing both problems'
      - 'writes nothing, the rules running before the first insert'
      - takes no organization number
      - two preferred addresses
      - 'refuses the create as ObjectInvalid, naming the problem'
      - 'writes nothing, the rules running before the first insert'
      - takes no organization number
      - >-
        an organization that breaks the structural rules and names a phone number type the tenant does not
        have (DEF-CRM-0108)
      - >-
        refuses the create as ObjectInvalid, naming the unknown phone number type with the structural problems
        in one answer
      - 'writes nothing, the rules running before the first insert'
      - takes no organization number
      - an element id that belongs to another tenant's organization
      - 'refuses to claim it, as ObjectInvalid'
      - rolls the organization row back with the rest of the transaction
      - 'leaves the organization number it took consumed, a gap'
      - leaves the other tenant's element where it is
      - getOrganization (integration)
      - an id two tenants both use
      - answers the first tenant its own organization
      - 'answers the second tenant its own organization, with that tenant''s elements'
      - an id the tenant does not have
      - answers undefined
      - listOrganizations (integration)
      - a page smaller than the tenant has organizations
      - 'answers the first organizations in number order, each with its collections'
      - 'answers the organizations that follow the cursor, and no cursor after the last of them'
      - answers the last organization on that page
      - a tenant whose organizations share their ids with another tenant
      - answers only the organizations of the tenant the execution acts in
      - updateOrganization (integration)
      - 'a replacement that edits one element, removes another and adds a third'
      - stores the field the replacement changed
      - keeps the organization number the create allocated
      - bumps the version of the record
      - removes the address the replacement left out
      - 'keeps the edited address under its own id, with the new value and a bumped version'
      - 'leaves the address the replacement sent back unchanged exactly as it was, version included'
      - adds the phone number the replacement named without an id
      - reads back exactly what the update answered
      - 'two opposite parent moves at the same instant (A under B, B under A)'
      - saves exactly one of the two moves
      - 'refuses the other naming parent_organization_id, as the cycle it would close, not a deadlock to retry'
      - leaves one of the two under the other and no cycle
      - a replacement that omits a collection and a field
      - empties the collection
      - clears the field
      - a replacement that removes the preferred address
      - promotes the next address of the tenant’s display order to preferred
      - leaves exactly one address preferred
      - a replacement that removes every address
      - 'leaves no preferred address, the organization having no address at all to prefer'
      - a replacement that marks two addresses preferred
      - 'refuses the update as ObjectInvalid, naming the problem'
      - leaves every row as it was
      - a replacement sent at a version the organization 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 organization of the owning tenant as it was
      - an organization made its own parent
      - refuses the update as ObjectInvalid
      - leaves every row as it was
      - an address whose coordinates are off the globe (DEF-CRM-0114)
      - 'refuses the update as ObjectInvalid, naming both fields, before the database check sees them'
      - leaves every row as it was
      - a parent the tenant does not have
      - refuses the update as ForeignKeyViolation
      - leaves every row as it was
      - an element id that belongs to another tenant's organization
      - 'refuses the update as ObjectInvalid, naming the element'
      - leaves every row as it was
      - deleteOrganization (integration)
      - an organization with addresses and phone numbers
      - answers the id it deleted
      - removes the organization and every owned element with it
      - 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 organization in place
      - an organization that other organizations name as their parent
      - refuses the delete as ForeignKeyViolation
      - an organization that contacts belong to
      - refuses the delete as ForeignKeyViolation
      - removes nothing
      - an organization locked against deletes
      - refuses the delete as IllegalOperation
      - removes nothing
      - an organization one of whose elements is locked against deletes
      - 'refuses the delete as IllegalOperation, naming the element that is locked'
      - removes nothing
      - organization ancestry (integration)
      - 'a three-level chain, read one by one'
      - 'answers the chapter its ancestors nearest first, their number and the top level'
      - 'answers the region its one ancestor, which is its top level'
      - answers the top-level organization none of the three properties
      - a list page
      - answers each organization on the page its own ancestors
      - an organization moved under another parent
      - starts with the leaf under old-top
      - answers the moved organization its new ancestry in the update response
      - 'answers the descendant, never saved again, its ancestry under the new parent'
      - 'leaves the descendant row unwritten: its version is still 1'
      - a parent removed
      - answers the detached organization none of the three properties
      - answers its descendant the detached organization as its only ancestor and top level
      - a move that would close a loop
      - refuses the move as ObjectInvalid naming the parent field
      - 'leaves the organization where it was, at its first version'
      - leaves the ancestry below it unchanged
      - 'still allows moving the bottom organization up, under the top'
      - a refused delete of an ancestor
      - refuses deleting an organization that has a child as ForeignKeyViolation
      - answers the child the same ancestry after the refusal
      - the same ids in two tenants
      - answers the leaf of this tenant this tenant's chain
      - answers the leaf of the other tenant that tenant's chain
      - ancestry a client sends
      - 'answers the computed ancestry on create, not what was sent'
      - >-
        accepts the record read back on replace, ancestry included, as the save that changes nothing it is
        (ruling D-05)
      - database round trips
      - answers the deepest organization all eleven ancestors
      - sends Postgres the same number of statements for eleven ancestors as for one
      - sends Postgres the same number of statements for a page of fourteen as for one organization
      - organization custom fields (integration)
      - a create refused for one invalid custom field
      - 'refuses it, naming the field'
      - leaves no organization and none of its collections
      - an update refused for one invalid custom field
      - leaves the standard field as it was
      - leaves the valid custom field of the same write 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 refused writer's field at its earlier value
      - a page of organizations with values
      - 'answers fifteen organizations, each with its custom fields'
      - sends the same number of statements for fifteen organizations as for one
      - organization logo through the asset pipeline (integration)
      - a create whose logo is a staging upload
      - stores the permanent reference in the organizations’ own legacy folder
      - reads back exactly the reference the create answered
      - writes the object into permanent storage with the type its bytes showed
      - 'attaches the lifecycle row to the organization, naming this object and its field'
      - a create that lets the server name the organization
      - 'gives the organization a UUIDv7 id, the same form a create with no file to copy gets'
      - 'files the logo under that 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 organization the logo was filed under
      - 'still takes the organization’s number from the tenant’s seed, which is no longer its id'
      - a replacement of the logo by another upload
      - stores a new reference at a key of its own
      - reads back the new reference
      - keeps the file the previous version of the record references
      - detaches the replaced file’s row and attaches the new one
      - an update naming an upload that is not there
      - 'refuses the update as ObjectInvalid, naming the logo field'
      - leaves the organization holding the logo it had
      - leaves the object of that logo in storage
      - leaves the lifecycle row of that logo attached
      - a replacement that sends the logo 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'
      - applies the edit the replacement did name
      - a write of the logo that did not go through the pipeline
      - 'refuses a direct update_organization_fields that changes the file reference, as ObjectInvalid'
      - leaves the organization holding the logo the pipeline did save
      - organization website and domain name (integration)
      - a create with a website and no domain name
      - stores the domain name derived from the website
      - stores the website as sent
      - 'a create with a website that is a page of a site, and no domain name'
      - 'stores the host of the website as the domain name, without its path'
      - 'stores the website as sent, path and all'
      - a create with a domain name sent with www and no website
      - stores the domain name without the leading www and in the casing sent
      - stores the website derived from the domain name
      - a create with a website that is not a URL
      - stores no domain name
      - stores the website as sent
      - a replacement that changes the website and leaves the domain name out
      - stores the domain name derived from the new website
      - a replacement that sends a website and a domain name naming different domains
      - stores the website as sent
      - 'stores the domain name as sent, without its leading www'
      - a re-save of the record as it was read
      - stores the same domain name again
      - stores the same website again
      - two organizations of one tenant whose domain names differ only in case
      - stores both organizations with the domain name each was sent
      - the same domain name in two tenants
      - stores the cleaned domain name in the second tenant's organization
      - stores the cleaned domain name in the first tenant's organization
      - does not let the first tenant read the second tenant's organization
      - organization_addresses.table (integration)
      - replace_owned_elements over an empty collection
      - read no address for the organization 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 organization 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 organization with no address at all
      - an address id that belongs to another organization of the tenant
      - 'refuses the claim as an invalid organization, naming the address that is not this organization’s'
      - leaves this organization’s addresses exactly as they were
      - leaves the other organization’s address where it is
      - the same organization id read and written in another tenant
      - answers no address for the organization id in a tenant that does not have the organization
      - 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 an organization 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
      - refuses two addresses of the same type
      - refuses an address with no type at all
      - 'refuses a latitude outside -90..90, naming the field'
      - 'refuses a longitude outside -180..180, naming the field'
      - 'names an address whose type the tenant does not have, by its element'
      - accepts the bounds of both ranges
      - organization_phone_numbers.table (integration)
      - replace_owned_elements over an empty collection
      - read no phone number for the organization 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 organization 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 organization with no phone number at all
      - a phone-number id that belongs to another organization of the tenant
      - 'refuses the claim as an invalid organization, naming the phone number that is not this organization’s'
      - leaves this organization’s phone numbers exactly as they were
      - leaves the other organization’s phone number where it is
      - the same organization id read and written in another tenant
      - answers no phone number for the organization id in a tenant that does not have the organization
      - 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 an organization 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
      - drops a blank number and formats a fixed slot as the legacy save did
      - 'formats a US number of any type, now that no type is the legacy `other_phone_numbers`'
      - refuses two numbers of the same type
      - organization compliance violations (integration)
      - the repository operations
      - 'replace and load the violations in the order given, the role name absent when unknown'
      - leave an organization with none absent from what is loaded
      - 'organization custom fields: an archived option (integration)'
      - holds gold on the organization 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 organization already holds
      - 'refuses an update that newly chooses the archived option, naming the field'
      - keeps the value of the organization whose update was refused
    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/organizations.queries.integration.spec.js
    declared_test_titles:
      - organizations read (integration)
      - getOrganization
      - 'answers the logo the asset pipeline made permanent, which the literal below then states'
      - 'answers the organization number the database allocated, which the literal below then states'
      - 'gave every address and phone number a generated id, one per slot, which the literal below then states'
      - >-
        answers the whole record of an organization with a value in every column, its collections, its
        ancestry and its violation
      - >-
        answers the whole record of an organization with nothing but a name: every empty column an absent key,
        both collections empty, no ancestry and no violations
      - answers undefined for an id the tenant does not have
      - 'answers the OTHER tenant’s own organization for the shared id, never the first tenant’s record'
      - answers undefined in a tenant that has no organization of that id at all
      - an organization with two alternates in each collection
      - >-
        stored both addresses and both phone numbers of the tenant’s own types, whatever order they were sent
        in
      - 'answers the main address first and the tenant’s own types after it, in the tenant’s display order'
      - 'answers the main phone number first and the tenant’s own types after it, in the tenant’s display order'
      - get_organization_fields
      - >-
        answers the organization row alone: every column of the whole record, and none of its collections,
        ancestry, custom fields or violations
      - answers undefined for an id the tenant does not have
      - answers the other tenant’s own row for the shared id
      - listOrganizations and list_organization_fields
      - >-
        answers the whole record of every organization of the first page, in number order, each with its
        collections
      - 'says where the first page ended, another organization following it'
      - >-
        answers the two organizations that remain on the page beginning where the first ended, the last of
        them with its ancestry
      - 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 organizations at all
      - 'leaves out the organizations of every other tenant, however many they hold'
      - >-
        answers the organization rows alone, without collections, ancestry or custom fields, through
        list_organization_fields
      - 'says where that page of rows ended, the fourth organization following it'
      - answers no rows and no next position for a page that begins past every number
      - the search filter of both lists
      - 'answers every organization of the tenant without it, so each case below is about the search'
      - answers the organizations whose name contains it
      - matches whatever the case
      - 'matches anywhere in the name, not only at its beginning'
      - answers an empty page for a name no organization carries
      - takes a percent sign as a character of the name and not as a wildcard
      - 'answers the whole tenant for a search of nothing but spaces: an empty search is no search'
      - 'pages within the search: the limit and the cursor count only the organizations it answers'
      - 'narrows the organization RECORD list the same way, collections and all'
      - reaches no organization of another tenant whose name matches
      - list_organization_links_above
      - 'answers every organization above the one it is asked about, each once with its own parent'
      - answers each shared ancestor once when two organizations of the same chain are asked about together
      - answers nothing for an organization that has no parent
      - answers nothing when it is asked about no organization at all
      - 'answers nothing in another tenant for an organization of the first, chain and all'
      - the ancestry derived for a record read
      - 'gives the organization in the middle of the chain its one ancestor, the top of the chain'
      - gives the top of the chain none of the three properties at all
      - load_compliance_violations
      - >-
        answers the organization’s violations in the order they were written, every field mapped and the bound
        a number
      - >-
        leaves the role name out entirely of the violation whose stored name is empty, rather than answering
        null
      - 'leaves an organization with no violation out of the map, though it was asked about'
      - answers an empty map when it is asked about no organization at all
      - answers an empty map in another tenant for an organization of the first
      - read_organization_contact_information_records
      - >-
        answers the main address by its id: whose it is and every value field a linked contact address
        resolves to
      - 'answers the main phone number by its id, its one value field the number'
      - 'answers every id it is asked about in one map, and leaves out an id the tenant has no record of'
      - answers an empty map for no id at all
      - answers nothing in another tenant for a record of the first
      - read_organization_records_by_type
      - >-
        answers the main address of the organization by its type: its id, and the organization’s and the
        type’s names
      - 'answers the fax number of the organization by its type, from the phone number types'
      - 'answers every reference under its key, each organization and type its own record'
      - 'answers no record, with both names, for an organization that has none of the type'
      - answers no type name and no record for a phone number type asked as an address type
      - answers no organization name and no record for an organization the tenant does not have
      - answers an empty map for no reference at all
      - >-
        answers the other tenant’s organization of the same id, which has no main address, and never the first
        tenant’s record
      - read_organization_region_addresses
      - >-
        answers the main address of the organization, every field the region is decided from, and no other
        slot
      - 'leaves an organization with no main address out of the map, though it was asked about'
      - answers an empty map when it is asked about no organization at all
      - answers an empty map in another tenant for an organization of the first
      - the preferred slot
      - 'takes the address from the one row that holds the preferred flag, not from the main type'
      - 'takes the main address of the same organization for the type slot, so the slot is what chooses the row'
      - lock_organization_region_targets
      - 'answers the region, the override flag and the fingerprint of each organization it is asked about'
      - answers nothing when it is asked about no organization at all
      - an organization the assignment has already decided
      - 'answers the deciding address the last assignment stored, which is what a repeat compares against'
      - an organization locked against updates
      - answered it as a target while it was not locked
      - 'leaves it out once it is locked against updates, the assignment never writing it'
      - lock_organization
      - >-
        answers the organization’s row as the TABLE holds it: every column, the unset ones null and the
        instants as Postgres sends them
      - 'refuses an id the tenant does not have, by RecordNotFound'
      - answers the other tenant’s own row for the shared id
      - lock_organization_for_compliance_recalculation
      - 'answers what the compliance rule decides from, and nothing else'
      - answers no type and no status for an organization held to no rule
      - answers undefined for an organization the tenant does not have
      - lock_organization_ids_after
      - 'answers the tenant’s next ids after the one given, in byte order, at most the limit'
      - answers nothing for a position past every id of the tenant
      - locked_organization_element_of
      - answers nothing while no element of the organization is locked
      - names the locked address by its collection’s label and its id
      - names the locked phone number by its collection’s label and its id
      - count_organizations
      - answers how many organizations the tenant holds
      - answers none for a tenant with no organization at all
      - the custom fields a record read answers
      - 'answers the whole record with every custom field in its wire form, keyed by the field’s key'
      - 'answers every field of an organization with no value as null, no key left out'
      - answers the same map for the organization on a list page
      - leaves the organizations of the tenants with no definitions with an empty map
      - organizations_held_to_compliance_rules
      - which of the organizations asked about it answers
      - answers one whose type holds it to compliance rules
      - leaves out one whose type holds it to none
      - leaves out one with no type at all
      - >-
        answers one no type holds to rules that still carries a compliance status, which a recalculation
        clears
      - 'answers one no type holds to rules that still carries violations, for the same reason'
      - 'answers every one of a mixed list that any of the three sides keeps, and nothing else'
      - the shape of the answer
      - 'answers in the order it was asked, not the order the database found them'
      - >-
        answers an id asked for twice once, so a change naming the same organization twice recalculates it
        once
      - 'answers nothing for an empty list, without going to the database'
      - leaves out an id no organization of the tenant has
      - another tenant’s organization
      - is answered when the read runs in that tenant (premise)
      - 'is left out when the read runs in this one, although its type holds it to rules'
      - what an organization’s delete would reach (integration)
      - counts every row the cascade would take from an organization that has one of each
      - 'answers zero for every name of an organization that holds nothing, rather than leaving the names out'
      - 'counts the organizations below one, which is what the database refuses the delete for'
      - 'counts the contacts affiliated with one, once each however many affiliations they hold to it'
      - 'counts no dependant of the organization BELOW one, which depends on nothing itself'
      - 'reports a locked organization as locked, which is the refusal the delete would make'
      - 'leaves out an id the tenant does not have, which is how a job rejects it as not found'
      - 'answers an empty map when it is given no ids, without reaching the database'
      - never answers another tenant's organization for an id only that tenant has
      - 'getOrganizationsByIds, the read of one organization made over a set (integration)'
      - >-
        answers the tenant’s organizations with those ids in id order, leaving out an id nobody has and
        another tenant’s
      - answers each organization exactly as the read of that one organization does
      - answers no organizations for no ids
    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/organization-addresses.table.integration.spec.js'
  'modules/entities/tests/integration/db/organization-custom-field-values.table.integration.spec.js'
  'modules/entities/tests/integration/db/organization-phone-numbers.table.integration.spec.js'
  'modules/entities/tests/integration/db/organizations-for-queries.view.integration.spec.js'
  'modules/entities/tests/integration/db/organizations.table.integration.spec.js'
  'modules/entities/tests/integration/services/persistence/organizations.archive.integration.spec.js'
  'modules/entities/tests/integration/services/persistence/organizations.field-rules.integration.spec.js'
  'modules/entities/tests/integration/services/persistence/organizations.integration.spec.js'
  'modules/entities/tests/integration/services/persistence/organizations.queries.integration.spec.js'
retries: none
proves_controls: []
product_intent: proposed
implementation_evidence: observed
narrative:
  status: enriched
---

# Organization aggregate persistence

## Verification Boundary

Organization persistence and its owned detail tables are exercised against PostgreSQL, including parent hierarchy and archived organizations.

## What This Suite Proves

Organization rows, addresses, phone numbers and custom values are written and read through their persistence boundaries. Field-rule tests distinguish writable values from computed values. Archive tests establish the stored archive stamp, retained affiliation context and restoration behavior. Read tests preserve permanent logo references and organization projections.

## What It Does Not Prove

Contact correction is outside this group. Compliance calculations and attention publishing are registered separately, even though an organization read includes their results. A direct service refusal does not establish that an unauthorized HTTP caller is denied; use the local API suite for that boundary.

## Level and Rationale

Parent references, workspace constraints and archive state need a real database. A mocked organization answer cannot establish that children still identify their parent, that persisted details follow the write, or that archive state survives a later independent read.

## Real Dependencies and Test Doubles

The organization persistence functions, execution context, PostgreSQL constraints and archive functions are real. Logo preparation uses the asset test seams where declared by the individual spec; remote storage availability is outside this proof. The prepared database supplies the reference classifications used by the fixtures.

## Must Not Be Doubled

Keep the organization row and detail tables, parent constraints and archive transactions real. A replacement hierarchy read would hide a broken reference, and a save-answer assertion without a fresh read would only establish mapping rather than durability.

## Fixtures and Isolation

The group creates named organizations and any required parent records. Table tests start from acceptable rows before violating a particular constraint. Cleanup follows dependency order so children and detail rows do not prevent parent cleanup; fixtures do not authorize changes to unrelated shared records.

## Determinism and Retries

Fixed hierarchy fixtures and explicit expected values make the graph stable. Archive instants are compared using the expectations the source declares rather than an invented wall-clock performance bound. Mocha stages preserve the original setup failure, and this register adds no retry policy.

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

Organization persistence does not prove a console editor, complete historical rendering or asynchronous compliance notice delivery. The organization field, archive and dependency-race findings remain open where they lack direct assertions. Public archive and delete settlement is assessed in the API lifecycle suite.
