---
id: DOM-MBR-MEM-003
title: Unit Affiliation
realises: CON-MBR-MEM-003
product_area: membership
module: memberships
revision: 1
status: draft
design_intent: proposed
implementation_evidence: observed
source_sha:
  services-membership: 5994c270e8a37a30f848e99ba5b5ee6b87c08893
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
owned_by: SVC-MBR-MEM-memberships
readable_by:
  - SVC-MBR-MEM-commerce-participant
  - SVC-MBR-MEM-snapshots
  - SVC-MBR-MEM-events
  - SVC-MBR-MEM-history
lifecycle: STATE-MBR-MEM-005
custom_fields: none
identity: >-
  Each dated unit connection has a stable identifier, one owning term and the
  membership of that term.
attributes:
  - name: term and membership
    kind: reference
    required: true
    computed: false
    notes: >-
      The composite term reference keeps the denormalized membership equal to
      the term’s owner.
  - name: unit and unit type
    kind: reference
    required: true
    computed: false
    notes: >-
      The unit is owned by Structure; persistence records its type alongside the
      reference.
  - name: starts on and ends on
    kind: date
    required: true
    computed: false
    notes: >-
      The span uses inclusive dates within term coverage. With no affiliation
      end, the term end bounds membership in the unit.
  - name: primary
    kind: enum
    required: true
    computed: false
    notes: The flag identifies a primary unit of its type for this span.
  - name: source
    kind: enum
    required: true
    computed: false
    notes: >-
      The source classifies the connection as application, suggested, assigned,
      transfer, staff, inherited or import.
  - name: dues references
    kind: collection
    required: false
    computed: false
    notes: >-
      Purchased unit dues retain product and order-line references; component
      fulfilment stamps the paying line.
  - name: transferred from
    kind: reference
    required: false
    computed: false
    notes: A successor can reference the affiliation it continues after a move.
  - name: in scope
    kind: computed
    required: false
    computed: true
    notes: >-
      Reads compute whether the officer’s evaluated grants reach the
      affiliation’s unit.
  - name: version
    kind: number
    required: true
    computed: true
    notes: >-
      Ending, transferring or changing the primary flag requires the version the
      caller read.
relationships:
  - to: DOM-MBR-MEM-002
    cardinality: many-to-one
    ownership: The term owns this dated connection.
    cross_service: false
  - to: DOM-MBR-MEM-003
    cardinality: many-to-one
    ownership: >-
      A successor references its predecessor; the reference is not constrained
      to one successor.
    cross_service: false
invariants:
  - id: INV-DOM-MBR-MEM-003-01
    statement: 'The last day, when present, cannot precede the first day.'
    enforced_at:
      - database
    provenance: observed
    source: >-
      services-membership:modules/memberships/db/membership-term-organizational-units.table.ts:membership_term_organizational_units_dates_check
  - id: INV-DOM-MBR-MEM-003-02
    statement: The membership reference must identify the owner of the referenced term.
    enforced_at:
      - database
    provenance: observed
    source: >-
      services-membership:modules/memberships/db/membership-term-organizational-units.table.ts
      (composite term key)
  - id: INV-DOM-MBR-MEM-003-03
    statement: >-
      Affiliations require a program configured to bind membership through
      affiliations.
    enforced_at:
      - service
      - database
    provenance: test
    source: >-
      services-membership:tests/acceptance/affiliations/MBR-ACC-0263.acceptance.ts#MBR-ACC-0263:
      Ending a member's only chapter is refused, a cancelled term's chapter
  - id: INV-DOM-MBR-MEM-003-04
    statement: One term cannot connect to the same unit twice on an overlapping day.
    enforced_at:
      - service
      - database
    provenance: test
    source: >-
      services-membership:modules/memberships/tests/integration/services/affiliations.integration.spec.ts#refuses
      the same unit twice on a day of a term (affiliation_overlap)
  - id: INV-DOM-MBR-MEM-003-05
    statement: >-
      Service validation checks allowed unit types, maximum counts across
      coverage and minimum counts from the operation’s effective date.
    enforced_at:
      - service
    provenance: test
    source: >-
      services-membership:modules/memberships/tests/unit/unit-rules.spec.ts#refuses
      fewer affiliations of a type than its minimum from the day the operation
      takes effect
  - id: INV-DOM-MBR-MEM-003-06
    statement: >-
      A unit type permitting only one primary cannot have overlapping primary
      spans on a term.
    enforced_at:
      - service
    provenance: observed
    source: >-
      services-membership:modules/memberships/services/affiliations/unit-rules.ts:affiliationSetProblems
  - id: INV-DOM-MBR-MEM-003-07
    statement: >-
      Database and service checks keep spans inside term dates; correction and
      termination trim or remove outlying rows.
    enforced_at:
      - service
      - database
    provenance: observed
    source: >-
      services-membership:modules/memberships/services/affiliations/affiliation-operations.ts:keepAffiliationsInsideTerm;
      db/migrations/20261018025135_membership_unit_bound.sql:membership_term_organizational_units_check_trigger
  - id: INV-DOM-MBR-MEM-003-08
    statement: >-
      A referenced unit cannot be deleted while any affiliation still names it,
      including historical closed spans.
    enforced_at:
      - service
      - database
    provenance: test
    source: >-
      services-membership:tests/e2e/s13-unit-merge.e2e.spec.ts#refuses to delete
      Savannah while affiliations name it (unit_has_affiliations)
  - id: INV-DOM-MBR-MEM-003-09
    statement: >-
      Adding or transferring directly into a unit with package dues requires an
      amendment and is refused by the current direct operations.
    enforced_at:
      - service
    provenance: test
    source: >-
      services-membership:modules/memberships/tests/integration/services/affiliations.integration.spec.ts#refuses
      to add a chapter that sells dues for the package
physical:
  - membership.membership_term_organizational_units
  - membership.membership_term_organizational_units_versions
evidence:
  - >-
    services-membership at 5994c270:
    modules/memberships/db/membership-term-organizational-units*.table.ts,
    membership-vocabularies.ts; services/affiliations/affiliation-operations.ts,
    unit-rules.ts, unit-merge.ts; services/access/unit-scope.ts;
    services/persistence/visibility-chains.ts; docs/migrations/membership-core
    (P2b), unit-merge, scope.
---

# Unit Affiliation

## What It Models

A unit affiliation explains where a member belongs during a term: a chapter, section or another permitted unit. Its dated span supports historical rosters, officer visibility and the identification of chapter dues.

## Identity and Attributes

The connection names its term, unit and unit type, its dates, primary flag and source. A purchased connection can record the dues product and paying line. Scope is computed for the reader rather than stored as an attribute of the affiliation.

## Relationships

Term ownership bounds the affiliation’s life. Transfer lineage references the earlier connection so moves remain traceable. Units remain owned by Structure, and the predecessor reference itself does not enforce a one-to-one relationship.

## Invariants

Date and reference constraints protect stored rows. The service checks minimums, maximums, permitted types and primary conflicts for the requested change. A direct change cannot bypass the dues requirement.

## How It Is Stored and Shared

Memberships owns connections and versions. Scope queries combine units with their ancestors; checkout writes the selected set and later stamps dues lines. Structure calls Membership during a merge, and snapshots preserve the dated unit hierarchy. Transfers publish start and end events; merge deletions, re-pointings and term trimming do not publish separate affiliation events.

## Open Design Questions

Multiple-primary reporting and preservation of a primary during a merge remain product questions. Portal self-service and affiliation imports are absent. An explicit end can retrospectively withdraw a row, so a promise that previously effective connections cannot be withdrawn would need additional enforcement.
