---
id: WF-MBR-STR-002
title: Merge a unit into a survivor and carry its members
trigger: An administrator combines a source unit with an active survivor.
product_area: membership
module: structure
revision: 1
status: draft
design_intent: confirmed
origin: >-
  Andrew's decision of 2026-10-18 (guided build acceptance, decision 1, "build the move now"):
  services-membership:docs/migrations/unit-merge/README.md#P2c: a unit merge moves its members' affiliations
implementation_evidence: observed
source_sha:
  services-membership: 5994c270e8a37a30f848e99ba5b5ee6b87c08893
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
inference: []
implements:
  - CAP-MBR-STR-003
moves:
  - STATE-MBR-STR-001
crosses:
  - SVC-MBR-STR-structure
idempotency: >-
  A repeated merge is rejected because the source is already merged. There is no request repeat key or
  retained response containing the original affiliation counts and leftovers.
concurrency: >-
  One workspace tree lock covers the complete merge. Source, survivor and affected memberships are locked,
  with membership locks acquired in identifier order. Concurrent writes to those memberships wait for the
  merge result.
hops:
  - 'n': 1
    actor_or_service: Structure API
    action: >-
      Authorize structure management and parse source, survivor, optional date, reason and version; the
      service supplies a child-move reason when absent.
    anchor: 'services-membership:modules/structure/aws/api/structure.routes.ts:structureRoutes'
    seam: sync
    on_failure: Reject malformed or unauthorized input before writes.
    left_inconsistent: No partial Membership write survives the failed transaction.
    observer: the caller
    provenance: observed
    source: 'services-membership:modules/structure/aws/api/structure-requests.ts:mergeBody'
  - 'n': 2
    actor_or_service: Structure service
    action: >-
      Lock the workspace tree, source and survivor, then validate source version when supplied, unmerged
      source, active distinct survivor outside the source branch and a nonfuture effective date.
    anchor: 'services-membership:modules/structure/services/tree/tree-edits.ts:mergeUnit'
    seam: sync
    on_failure: Reject the merge and keep all existing records.
    left_inconsistent: No partial Membership write survives the failed transaction.
    observer: the caller
    provenance: test
    source: >-
      services-membership:modules/structure/tests/unit/services/tree-edits.spec.ts#the tree edits › merge ›
      refuses a survivor below the merged unit
  - 'n': 3
    actor_or_service: Memberships module affiliation step
    action: >-
      Ask Memberships to lock and reread affected memberships and plan affiliations and identity changes
      before mutation; compare resulting rules with existing violations.
    anchor: 'services-membership:modules/memberships/services/affiliations/unit-merge.ts'
    seam: sync
    on_failure: Refuse the entire merge with up to twenty named problems and the total refusal count.
    left_inconsistent: No partial Membership write survives the failed transaction.
    observer: the caller
    provenance: test
    source: >-
      services-membership:modules/structure/tests/integration/services/tree/unit-merge.integration.spec.ts#the
      tree edits affiliations bind › a merge an affiliation cannot follow › is refused 409
      affiliations_not_movable, naming the membership and unit_minimum
  - 'n': 4
    actor_or_service: Structure service
    action: Validate each child under the survivor and write its new parent and dated period.
    anchor: 'services-membership:modules/structure/services/tree/tree-edits.ts:validatePlacement'
    seam: sync
    on_failure: Any rejected child rolls back all child moves and the whole merge.
    left_inconsistent: No partial Membership write survives the failed transaction.
    observer: the caller
    provenance: test
    source: >-
      services-membership:modules/structure/tests/integration/services/persistence/organizational-units.integration.spec.ts#organizational
      units in the database › merge › moves the children under the survivor, marks the unit merged and lists
      what it left
  - 'n': 5
    actor_or_service: Structure service
    action: >-
      Set source standing to merged and retain its survivor reference without changing its own parent or open
      period.
    anchor: 'services-membership:modules/structure/services/persistence/organizational-units.ts:setUnitStatus'
    seam: sync
    on_failure: Failure aborts the transaction.
    left_inconsistent: No partial Membership write survives the failed transaction.
    observer: unit versions
    provenance: observed
    source: 'services-membership:modules/structure/services/tree/tree-edits.ts:mergeUnit'
  - 'n': 6
    actor_or_service: Memberships module affiliation step
    action: >-
      Apply the planned transfer, repointing, ending, withdrawal and identity changes with attributed
      membership lifecycle records.
    anchor: 'services-membership:modules/memberships/services/affiliations/unit-merge.ts'
    seam: sync
    on_failure: 'Any persistence or deferred validation refusal rolls back source, children and members together.'
    left_inconsistent: No partial Membership write survives the failed transaction.
    observer: each membership's history
    provenance: documented
    source: 'services-membership:docs/migrations/unit-merge/README.md#The behaviour'
  - 'n': 7
    actor_or_service: Structure service
    action: >-
      Rebuild ancestry and depths, publish reparenting for relocated children, collect source leftovers and
      commit.
    anchor: 'services-membership:modules/structure/services/tree/tree-edits.ts:mergeUnit'
    seam: sync
    on_failure: 'Rebuild, event or read-back failure prevents the full merge from committing.'
    left_inconsistent: No partial Membership write survives the failed transaction.
    observer: the caller's answer
    provenance: test
    source: >-
      services-membership:modules/structure/tests/unit/services/tree-edits.spec.ts#the tree edits › merge ›
      states membership.unit.reparented for each child that moved under the survivor, naming the merged unit
  - 'n': 8
    actor_or_service: Structure service
    action: 'After commit, emit child reparent and source merge telemetry with the affiliation counts.'
    anchor: 'services-membership:modules/structure/services/tree/tree-edits.ts:mergeUnit'
    seam: sync
    on_failure: Telemetry has no local catch; library exception behavior remains unknown.
    left_inconsistent: The committed merge stands even if telemetry or the final caller response fails.
    observer: execution telemetry
    provenance: unknown
    source: FND-MBR-STR-051
  - 'n': 9
    actor_or_service: 'Staff, outside this flow'
    action: >-
      Staff use the returned leftovers to arrange Accounting, link or committee follow-up outside this
      implementation.
    anchor: 'services-membership:modules/structure/services/tree/tree-edits.ts:leftBehind'
    seam: sync
    on_failure: 'No composed follow-up, reminder or compensation runs automatically.'
    left_inconsistent: >-
      Finance settings, dues extensions, links and committee attachments can remain associated with the merged
      source.
    observer: the merge answer only
    provenance: inferred
    source: 'services-membership:modules/structure/services/tree/tree-edits.ts:leftBehind'
branches:
  - An inactive source is eligible when the survivor is active.
  - Closed-before-date affiliations and cancelled-term affiliations remain historical records.
  - Descendant affiliations retain their unit identities while those units move.
waits:
  - The merge waits for workspace-tree and affected-membership locks.
evidence:
  - >-
    services-membership at 5994c270: modules/structure/services/tree/tree-edits.ts (mergeUnit, leftBehind);
    modules/memberships/services/affiliations/unit-merge.ts; docs/migrations/unit-merge/README.md;
    tests/e2e/s13-unit-merge.e2e.spec.ts.
controls: [TC-AVL-002, TC-AVL-003, TC-AVL-004, TC-AZ-001, TC-AZ-002, TC-AZ-003, TC-AZ-004, TC-AZ-005, TC-AZ-006, TC-CHG-001, TC-CHG-002, TC-CHG-003, TC-CHG-004, TC-CRY-001, TC-CRY-002, TC-CRY-003, TC-DAT-001, TC-DAT-002, TC-DAT-003, TC-DAT-007, TC-IAM-001, TC-IAM-002, TC-IAM-004, TC-IAM-005, TC-IAM-006, TC-LOG-001, TC-LOG-002, TC-LOG-003, TC-LOG-004, TC-LOG-005, TC-MON-001, TC-MON-002, TC-MON-004, TC-PI-001, TC-PI-002, TC-PI-003, TC-PI-004, TC-PI-005, TC-PRV-001, TC-PRV-002, TC-SEC-001, TC-SEC-004, TC-TEN-001, TC-TEN-002, TC-TEN-003, TC-TEN-004]
---

# Merge a unit into a survivor and carry its members

## What It Does

A merge combines structural and membership changes in one transaction. Memberships plans affected affiliations and identity bindings before Structure moves children or marks the source. Finance and committee changes are outside that transaction and appear as follow-up information.

## The Flow

```mermaid
sequenceDiagram
  actor Staff
  participant Structure
  participant Memberships
  participant Database
  Staff->>Structure: combine source and survivor
  Structure->>Database: lock tree and both units
  Structure->>Memberships: plan under membership locks
  Memberships-->>Structure: writes and refusal list
  alt no refusals
    Structure->>Database: move children and mark source merged
    Structure->>Memberships: apply affiliation and identity plan
    Structure->>Database: rebuild, record child events and commit
    Structure-->>Staff: counts and leftovers
  else refused
    Structure-->>Staff: problems with no committed change
  end
```

## When Something Fails

A newly introduced affiliation violation or an unplaceable child blocks the complete merge. No partial membership transfer commits. After commit, telemetry cannot reverse the result; follow-up depends on staff retaining and acting on the response.

## Open Design Questions

Product must decide how the merged source appears in current structure and which changes subscribers should receive. Finance and committee leftovers also need an accountable follow-up process.
