---
id: CAP-CRM-ENT-017
title: Configure affiliation types and contact roles
product_area: crm
module: entities
revision: 1
status: approved
product_intent: proposed
implementation_evidence: partially-observed
source_sha:
  services-crm: 7ea48a414c42eade10d60753b8bcc323b1dd726f
  react-console: da63075876b2c5f4a6af07d457bb24ea8bf4c0c7
intent_sources:
  - 'OQ-CRM-ENT-002 decided 2026-10-22 by Andrew: B (classification types and the role catalogue stay in Entities)'
  - 'services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.md#D-11'
  - 'services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.md#M2-D7'
  - 'services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.md#M2-D10'
  - 'services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.plan.md#A4'
  - 'services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.plan.md#A5'
  - >-
    services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.plan.md#A10
  - >-
    services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.plan.md#A11
  - >-
    services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.plan.md#A17
  - >-
    services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.plan.md#A23
purpose: >-
  Administrators define the relationship categories and responsibilities used by
  the tenant.
outcomes:
  - Every affiliation and role names a type or role the tenant maintains.
  - A type or role in use cannot be deleted.
  - The workspace default type is chosen only by an administrator.
actors:
  - ACT-CRM-001
  - ACT-CRM-003
  - ACT-CRM-004
surfaces:
  - console
  - api
  - portal
preconditions: []
postconditions: []
permissions:
  - key: crm.contact-roles.read
    where_checked: api
    scope: tenant
  - key: crm.contact-roles.create
    where_checked: api
    scope: tenant
  - key: crm.contact-roles.update
    where_checked: api
    scope: tenant
  - key: crm.contact-roles.delete
    where_checked: api
    scope: tenant
  - key: crm.affiliation-types.read
    where_checked: api
    scope: tenant
  - key: crm.affiliation-types.create
    where_checked: api
    scope: tenant
  - key: crm.affiliation-types.update
    where_checked: api
    scope: tenant
  - key: crm.affiliation-types.delete
    where_checked: api
    scope: tenant
  - key: crm.affiliation-types.set-default
    where_checked: api
    scope: tenant
rules:
  - id: BR-CAP-CRM-ENT-017-01
    proved_by: [ATS-CRM-ENT-017-01, TST-CRM-ENT-786]
    statement: >-
      Contact roles and affiliation types require trimmed nonblank names and
      reject blank input without creating a record.
    kind: validation
    enforced_at:
      - api
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#answers
      400 naming the field for a create whose name is nothing but spaces
    origin: none
  - id: BR-CAP-CRM-ENT-017-02
    proved_by: [ATS-CRM-ENT-017-02, TST-CRM-ENT-786]
    statement: >-
      Role and affiliation-type ids are unique immutable lowercase configuration
      ids up to 63 characters, may derive from the name and cannot use Rhythm’s
      reserved prefix.
    kind: validation
    enforced_at:
      - api
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#answers
      400 for an id that is not a configuration id, saying what an id may hold
    origin: none
  - id: BR-CAP-CRM-ENT-017-03
    proved_by: [ATS-CRM-ENT-011-16, ATS-CRM-ENT-017-03, ATS-CRM-ENT-017-14, ATS-CRM-ENT-017-15, TST-CRM-ENT-776, TST-CRM-ENT-786, TST-CRM-ENT-787]
    statement: >-
      Contact roles referenced by current or historical holders or
      organization-type compliance rules cannot be deleted, with holder counts
      or dependent type names included in the refusal.
    kind: validation
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-roles.integration.spec.js#refuses
      it with the legacy words, naming the organization types whose rules depend
      on it
    origin: >-
      services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.plan.md#A10
  - id: BR-CAP-CRM-ENT-017-04
    proved_by: [ATS-CRM-ENT-015-13, ATS-CRM-ENT-017-04, ATS-CRM-ENT-017-16, ATS-CRM-ENT-017-19, TST-CRM-ENT-776]
    statement: >-
      Creating and editing roles or affiliation types is available to users,
      while deleting either requires administrator authority.
    kind: permission
    enforced_at:
      - api
    must_prove: true
    provenance: test
    source: >-
      react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0196.spec.ts#CRM-ACC-0196
    origin: 'services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.md#M2-D7'
  - id: BR-CAP-CRM-ENT-017-05
    proved_by: [ATS-CRM-ENT-017-05, TST-CRM-ENT-786]
    statement: >-
      Affiliation types cannot be deleted while referenced by an affiliation or
      selected as the workspace default.
    kind: validation
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#refuses
      the delete (409) while an affiliation of a contact is of that type
    origin: none
  - id: BR-CAP-CRM-ENT-017-06
    proved_by: [ATS-CRM-ENT-009-23, ATS-CRM-ENT-017-06, ATS-CRM-ENT-017-13, ATS-CRM-ENT-017-18, ATS-CRM-ENT-017-19, ATS-CRM-ENT-019-09, TST-CRM-ENT-786]
    statement: >-
      The workspace default affiliation type is Other until configured, must
      exist in the tenant, supplies Organization-command affiliations and uses
      separate read and administrator set-default permissions.
    kind: permission
    enforced_at:
      - api
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#refuses
      a caller who may only read affiliation types the change of the default
      (403)
    origin: >-
      services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.plan.md#A11
  - id: BR-CAP-CRM-ENT-017-07
    proved_by: [ATS-CRM-ENT-006-16, ATS-CRM-ENT-017-07, ATS-CRM-ENT-017-12, ATS-CRM-ENT-017-17, ATS-CRM-ENT-018-14, TST-CRM-ENT-776, TST-CRM-ENT-786, TST-CRM-ENT-787]
    statement: >-
      Affiliation default mappings use tenant source and destination types of
      the same address or phone kind, allow each destination once per kind and
      at most 20 mappings, preserve omitted mappings on replacement and clear
      them for null.
    kind: validation
    enforced_at:
      - api
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/affiliation-types.integration.spec.js#keeps
      the mappings, as an omitted collection means unchanged
    origin: >-
      services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.plan.md#A17
  - id: BR-CAP-CRM-ENT-017-08
    proved_by: [ATS-CRM-ENT-017-08, TST-CRM-ENT-786, TST-CRM-ENT-787]
    statement: >-
      Contact-role reorder includes every tenant role once, assigns consecutive
      positions, rejects missing or unknown roles without partial movement,
      permits moving locked roles and lists never-ordered roles alphabetically.
    kind: validation
    enforced_at:
      - api
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/contact-roles.e2e.spec.js#answers
      400 for a reorder that leaves a role of the tenant out, naming it
    origin: >-
      services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.plan.md#A5
  - id: BR-CAP-CRM-ENT-017-09
    proved_by: []
    statement: >-
      The product should provide editable contact-role and affiliation-type
      descriptions in the shared configuration lists and forms.
    kind: policy
    enforced_at:
      - console
    must_prove: false
    provenance: documented
    source: 'services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.md#D-11'
    origin: 'services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.md#D-11'
  - id: BR-CAP-CRM-ENT-017-10
    proved_by: [ATS-CRM-ENT-017-11, ATS-CRM-ENT-017-19, TST-CRM-ENT-786]
    statement: >-
      Tenant portal users can read contact-role lists paged in name order using
      an opaque cursor.
    kind: permission
    enforced_at:
      - api
    must_prove: false
    provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/contact-roles.e2e.spec.js#answers
      a portal user of the tenant
    origin: none
  - id: BR-CAP-CRM-ENT-017-11
    proved_by: [ATS-CRM-ENT-017-10, TST-CRM-ENT-776]
    statement: >-
      Deactivated affiliation types keep their dataset columns, with an inactive
      label.
    kind: policy
    enforced_at: []
    must_prove: true
    provenance: test
    source: >-
      react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0199.spec.ts#CRM-ACC-0199
    origin: >-
      services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.md#M2-D10
scenarios:
  - id: SC-CAP-CRM-ENT-017-01
    given: An administrator in Settings.
    when: >-
      She adds "Term Treasurer" with a description, reorders it and deactivates
      it.
    then: 'The role is listed in its place, inactive, and pickers stop offering it.'
    provenance: test
    source: >-
      react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0197.spec.ts#CRM-ACC-0197
  - id: SC-CAP-CRM-ENT-017-02
    given: Employer with address and phone default links.
    when: The administrator sets it as the workspace default.
    then: The Organization field creates Employer affiliations.
    provenance: test
    source: >-
      react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0199.spec.ts#CRM-ACC-0199
edge_cases: []
constraints: []
depends_on: []
related:
  - CAP-CRM-ENT-006
  - CAP-CRM-ENT-009
  - CAP-CRM-ENT-011
  - CAP-CRM-ENT-012
  - CAP-CRM-ENT-015
  - CAP-CRM-ENT-018
  - CAP-CRM-ENT-019
journeys:
  - JNY-CRM-ENT-004
open_questions: []
feature: FEAT-CRM-ENT-06
concepts:
  - CON-CRM-ENT-003
evidence:
  - >-
    `services-crm` at `7ea48a414c42eade10d60753b8bcc323b1dd726f`: examined
    `aws/api/contact-roles.routes.ts`, `contact-roles.schemas.ts`, `contact-role-api-mapper.ts`,
    `affiliation-types.routes.ts`, `affiliation-types.schemas.ts`, `affiliation-type-api-mapper.ts`,
    `crm-settings.routes.ts`, `services/persistence/contact-roles.ts`, `affiliation-types.ts`,
    `crm-settings.ts`, `db/contact-roles.table.ts`, `affiliation-types.table.ts`,
    `affiliation-type-contact-info-defaults.table.ts`, `aws/crm.service.ts`; tests
    `e2e/contact-roles.e2e.spec.js`, `e2e/affiliation-types.e2e.spec.js`,
    `integration/services/persistence/contact-roles.integration.spec.js`,
    `affiliation-types.integration.spec.js`, `unit/api/record-lock-api-mapper.spec.js`.
  - >-
    `react-console` at `da63075876b2c5f4a6af07d457bb24ea8bf4c0c7`: examined route names from
    `lib/routes/__tests__/routes.test.ts`; inventory RUL-CRM-AFF-033 to 041; journeys CRM-ACC-0196
    to 0200.


    The permission catalogue also publishes configuration bulk keys. FND-CRM-ENT-013 records that
    these keys have no corresponding route in this module.
approved_by: Andrew
approved_at: 2026-10-22
controls: [TC-AZ-001, TC-AZ-002, TC-AZ-003, TC-AZ-004, TC-AZ-005, TC-AZ-006, TC-DAT-001, TC-DAT-003, TC-LOG-004, TC-LOG-005, TC-PI-001, TC-PI-003, TC-TEN-001, TC-TEN-002, TC-TEN-003]
narrative:
  status: verified
  review: NARRATIVE-REVIEW-2026-10-23
narrative_sources:
  - section: Why It Works This Way
    provenance: inferred
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#answers
      400 naming the field for a create whose name is nothing but spaces;
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#answers
      400 for an id that is not a configuration id, saying what an id may hold;
      services-crm:modules/entities/tests/integration/services/persistence/contact-roles.integration.spec.js#refuses
      it with the legacy words, naming the organization types whose rules depend
      on it;
      react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0196.spec.ts#CRM-ACC-0196;
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#refuses
      the delete (409) while an affiliation of a contact is of that type;
      services-crm:modules/entities/tests/e2e/affiliation-types.e2e.spec.js#refuses
      a caller who may only read affiliation types the change of the default
      (403);
      services-crm:modules/entities/tests/integration/services/persistence/affiliation-types.integration.spec.js#keeps
      the mappings, as an omitted collection means unchanged;
      services-crm:modules/entities/tests/e2e/contact-roles.e2e.spec.js#answers
      400 for a reorder that leaves a role of the tenant out, naming it;
      services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.md#D-11;
      services-crm:modules/entities/tests/e2e/contact-roles.e2e.spec.js#answers
      a portal user of the tenant;
      react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0199.spec.ts#CRM-ACC-0199
    note: >-
      Operational rationale inferred from the cited behavior; no personal motive
      or historical decision is asserted.
repositories:
  - repo: services-crm
    role: owner
    paths:
      - modules/configuration-types/
      - modules/entities/
      - docs/milestones/CRM-M2-affiliations-roles-compliance.md
      - modules/entities/tests/e2e/affiliation-types.e2e.spec.js
      - modules/entities/tests/e2e/contact-roles.e2e.spec.js
      - >-
        modules/entities/tests/integration/services/persistence/affiliation-types.integration.spec.js
      - >-
        modules/entities/tests/integration/services/persistence/contact-roles.integration.spec.js
  - repo: react-console
    role: console
    paths:
      - components/configuration/
      - acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0196.spec.ts
      - acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0197.spec.ts
      - acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0199.spec.ts
exercised_by: [FAS-CRM-ENT-06-01, FAS-CRM-ENT-06-03, FAS-CRM-ENT-06-04, FAS-CRM-ENT-06-05]
suites: [TST-CRM-ENT-776, TST-CRM-ENT-786, TST-CRM-ENT-787]
---

# Configure affiliation types and contact roles

## Purpose

Staff need shared meanings for organizational ties and responsibilities before assigning them to people. This capability maintains the affiliation-type and contact-role catalogues, sets the default relationship type for the Organization command, and configures shared-detail mappings. It separates changing that vocabulary from rewriting relationships that already use it.

## How It Works

Authorized users create or edit named roles and affiliation types with stable configuration identities. The supplied identity follows the configuration grammar and stays immutable. Contact roles can be ordered as a complete catalogue, while delete authority is separate from ordinary maintenance. Existing holders and organization-type policy references can block role deletion.

Affiliation types can carry same-kind address or phone mappings with distinct destination types. Omitted mappings on replacement remain; an explicit clearing value removes them. The workspace default initially uses Other and can be changed with separate administrator authority. Existing affiliations and the selected default block type deletion.

Deactivating an affiliation type retains its dataset columns with an inactive label. The description-editing requirement remains recorded as proposed behavior rather than being proved solely by the presence of configuration lists. Portal role-list reading has its own supported paged path.

## Example

An administrator creates Member Firm with a mapping from organization main phone to contact work phone and selects it as the workspace default. A subsequent Organization command creates a tie of that kind. Deleting Member Firm is refused while it remains default or referenced. Switching the default and deactivating it retires new selection without rewriting the affiliations already bearing that meaning.

In the Member Firm configuration journey, the administrator inspects the new type and its main-phone-to-work-phone mapping before using it. A mapping with two sources targeting Work phone is refused rather than selecting whichever source happens to run last. Resetting the workspace default requires the dedicated set-default authority, so a role-catalogue editor cannot infer that authority from ordinary maintenance access. After the administrator selects the type, a coordinator's Organization command can create the corresponding tie. Changing the mapping later leaves that saved person's choices intact. The administrator verifies retirement by retaining existing affiliations and excluding the deactivated choice from new selection.

## Why It Works This Way

Our reading is that immutable catalogue identities let a name or description improve without breaking references to the underlying business meaning. Reference-aware deletion protects historical holders and configured policies, which still need an understandable role or relationship type. Separate default authority limits a choice that influences later record work across the workspace.

Unique mapping destinations prevent two competing organization sources from trying to supply one contact slot. Same-kind checks keep address and phone purposes distinct. Preserving omitted mappings protects clients that do not administer that collection from silently clearing it. Deactivation offers retirement while retaining interpretability; it is not a promise that every saved-query execution path has been proven after retirement.

## Relationships

Maintain a person’s organizational relationships uses the catalogue and workspace default. Shared contact points consumes default mappings only at its stated eligible moments. Role assignment and organization compliance consume Contact Role definitions. CRM Settings holds the default choice, and Platform datasets expose relationship columns. Catalogue route hosting does not transfer product ownership away from Entities.
