---
id: CAP-CRM-REG-008
title: Keep placements aligned with address changes
feature: FEAT-CRM-REG-03
product_area: crm
module: regions
revision: 2
status: draft
product_intent: proposed
implementation_evidence: observed
supersedes: []
source_sha:
  services-crm: a8d1364f1dd44b723bb5f9acba12f983e9ec8d69
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
intent_sources:
  - >-
    services-crm:docs/migrations/crm-regions/README.md#2026-10-12 — Final acceptance (second pass), decision
    2026-10-11: the region follows an address change after the save, through the change-event queue
  - >-
    services-crm:docs/migrations/crm-regions/README.md#Behaviour changes (Andrew's requirement for the port:
    replace or remove stale calculated assignments)
  - >-
    services-crm:docs/migrations/crm-regions/README.md#2026-10-12 — Final acceptance (second pass), decision
    2026-10-12: the assignment bumps a record's version only when its region changes
  - 'services-crm:docs/milestones/CRM-M1-contacts-and-organizations.plan.md#CRM-ACC-0023'
  - 'services-crm:docs/milestones/CRM-M1-contacts-and-organizations.plan.md#CRM-ACC-0058'
purpose: Rhythm updates a record’s regional placement when the address used for that placement changes.
outcomes:
  - Calculated placements follow deciding-address changes and clear when coverage is lost.
  - Remembering an unchanged placement does not invalidate the user’s saved version.
concepts:
  - CON-CRM-REG-003
actors:
  - ACT-CRM-005
  - ACT-CRM-001
surfaces:
  - job
preconditions: []
postconditions: []
permissions: []
rules:
  - id: BR-CAP-CRM-REG-008-01
    proved_by: [ATS-CRM-REG-006-10, ATS-CRM-REG-008-01, TST-CRM-REG-021, TST-CRM-REG-047, TST-CRM-REG-058, TST-CRM-REG-062]
    statement: >-
      Automatic placement follows a committed contact or organization save asynchronously, so a create
      response does not yet contain the calculated region.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/regions/tests/integration/services/region-assignment/region-assignment.integration.spec.js#initial
      assignment › answers no region from the create itself, because the assignment is a reaction to the
      committed save
    origin: >-
      services-crm:docs/migrations/crm-regions/README.md#2026-10-12 — Final acceptance (second pass), decision
      2026-10-11: the region follows an address change after the save, through the change-event queue
  - id: BR-CAP-CRM-REG-008-02
    proved_by: [ATS-CRM-REG-008-02, TST-CRM-REG-058, TST-CRM-REG-060, TST-CRM-REG-066]
    statement: Changing a deciding address into another rule’s area changes the record to that rule’s region.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/regions/tests/integration/services/region-assignment/region-assignment.integration.spec.js#moving
      › moves a contact from region A to region B when its address moves
    origin: 'services-crm:docs/milestones/CRM-M1-contacts-and-organizations.plan.md#CRM-ACC-0023'
  - id: BR-CAP-CRM-REG-008-03
    proved_by: [ATS-CRM-REG-008-03, TST-CRM-REG-058, TST-CRM-REG-066]
    statement: >-
      Removing a previously deciding address or moving it outside all matching coverage clears the calculated
      region.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/regions/tests/integration/services/region-assignment/region-assignment.integration.spec.js#moving
      › clears the region when the deciding address is removed
    origin: >-
      services-crm:docs/migrations/crm-regions/README.md#Behaviour changes (Andrew's requirement for the port:
      replace or remove stale calculated assignments)
  - id: BR-CAP-CRM-REG-008-04
    proved_by: [ATS-CRM-REG-008-04, TST-CRM-REG-058]
    statement: Editing an address outside the configured deciding source does not change regional placement.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/regions/tests/integration/services/region-assignment/region-assignment.integration.spec.js#moving
      › ignores a change to an address that does not decide the region
    origin: none
  - id: BR-CAP-CRM-REG-008-05
    proved_by: [ATS-CRM-REG-003-08, ATS-CRM-REG-008-05, ATS-CRM-REG-008-15, TST-CRM-REG-057, TST-CRM-REG-058]
    statement: >-
      Placement uses the configured preferred or typed address and follows a newly preferred address when that
      source is selected.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/regions/tests/integration/services/region-assignment/region-assignment.integration.spec.js#moving
      › follows the address that became preferred
    origin: none
  - id: BR-CAP-CRM-REG-008-06
    proved_by: [ATS-CRM-REG-008-06, TST-CRM-REG-058]
    statement: A record that has never had a deciding address retains a region supplied manually.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/regions/tests/integration/services/region-assignment/region-assignment.integration.spec.js#initial
      assignment › keeps a region the client sends with a contact that has no address (nothing decides it)
    origin: >-
      services-crm:docs/migrations/crm-regions/README.md#Behaviour changes, Not changed (kept as Rhythm 1.0
      did; accepted by Andrew 2026-10-08)
  - id: BR-CAP-CRM-REG-008-07
    proved_by: [ATS-CRM-REG-008-07, ATS-CRM-REG-008-16]
    statement: >-
      Assignment skips records locked against updates and treats records deleted before processing as
      successful work with nothing to change.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: observed
    source: >-
      services-crm:modules/regions/tests/integration/services/region-assignment/region-assignment-delivery.integration.spec.js#a
      record deleted while its event was in flight › succeeds with nothing to do, because a record that is
      gone has no region to decide;
      services-crm:modules/regions/services/region-assignment/region-assignment.ts:reconcileRegionAssignments
    origin: none
  - id: BR-CAP-CRM-REG-008-08
    proved_by: [ATS-CRM-REG-008-08, TST-CRM-REG-056]
    statement: >-
      Processing reads current committed record state, making duplicate, late and out-of-order events converge
      on the same result without repeat writes.
    kind: invariant
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/regions/tests/integration/services/region-assignment/region-assignment-delivery.integration.spec.js#two
      messages delivered out of order › ends in the region of the address the contact has now, not of the one
      the older message described
    origin: >-
      services-crm:docs/migrations/crm-regions/README.md#2026-10-12 — Final acceptance (second pass), decision
      2026-10-11: the region follows an address change after the save, through the change-event queue
  - id: BR-CAP-CRM-REG-008-09
    proved_by: [ATS-CRM-REG-008-09, TST-CRM-REG-056]
    statement: An assignment write does not trigger another assignment of its own.
    kind: invariant
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/regions/tests/integration/services/region-assignment/region-assignment-delivery.integration.spec.js#one
      delivery of one address change › is not called again by its own write, so an assignment cannot set off
      another
    origin: none
  - id: BR-CAP-CRM-REG-008-10
    proved_by: [ATS-CRM-REG-008-10, TST-CRM-REG-056, TST-CRM-REG-058]
    statement: >-
      Assignment advances the version and modification stamp only when the region changes, preserving both
      when only the remembered address changes.
    kind: policy
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/regions/tests/integration/services/region-assignment/region-assignment-delivery.integration.spec.js#a
      deciding-address change that leaves the region where it was › bumps no version, so the contact stands at
      the version its save answered
    origin: >-
      services-crm:docs/migrations/crm-regions/README.md#2026-10-12 — Final acceptance (second pass), decision
      2026-10-12: the assignment bumps a record's version only when its region changes
  - id: BR-CAP-CRM-REG-008-11
    proved_by: [ATS-CRM-REG-008-11, TST-CRM-REG-028, TST-CRM-REG-058]
    statement: >-
      An organization address change reconciles linked contacts and can update their regions without writing
      their contact addresses.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/regions/tests/integration/services/region-assignment/region-assignment.integration.spec.js#a
      linked address (docs/affiliations/README.md §0b.3, the fan-out) › moves the linked contact when its
      organization’s address moves, though no contact row was written
    origin: none
  - id: BR-CAP-CRM-REG-008-12
    proved_by: [ATS-CRM-REG-008-12]
    statement: >-
      Linked addresses supply placement only through effective affiliations, and affiliation expiry alone does
      not trigger regional reconciliation.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: inferred
    source: >-
      services-crm:modules/entities/services/persistence/contacts.ts:readContactRegionAddresses;
      services-crm:modules/entities/services/affiliations/affiliation-date-reconciliation.ts
    origin: none
    note: >-
      Effective-affiliation filtering removes the linked address at read time; date reconciliation does not
      emit an address change to invoke the region handlers. This explains expiry alone, not a later
      organization address event.
  - id: BR-CAP-CRM-REG-008-13
    proved_by: [ATS-CRM-REG-008-13, TST-CRM-REG-056]
    statement: >-
      Failed assignment leaves placement unchanged and the queue can deliver the work again for successful
      processing.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/regions/tests/integration/services/region-assignment/region-assignment-delivery.integration.spec.js#a
      delivery that fails and is made again › assigns the region on the next delivery of the same message
    origin: none
  - id: BR-CAP-CRM-REG-008-14
    proved_by: [ATS-CRM-REG-008-14, TST-CRM-REG-057, TST-CRM-REG-058]
    statement: 'Each workspace uses only its own policy, and missing settings disable placement.'
    kind: invariant
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/regions/tests/integration/services/region-assignment/region-assignment-rules.integration.spec.js#tenant
      isolation, with rules on both sides › leaves a contact at that same address unassigned here, because the
      rule that covers it is the neighbour’s
    origin: none
scenarios:
  - id: SC-CAP-CRM-REG-008-01
    given: Enabled rules cover a new member’s deciding address.
    when: The coordinator saves the contact and then reads it after processing.
    then: The calculated region appears after the create response.
    provenance: test
    source: 'react-console:acceptance/crm/playwright/journeys/configuration/CRM-ACC-0357.spec.ts#CRM-ACC-0357'
  - id: SC-CAP-CRM-REG-008-02
    given: A contact’s deciding address currently matches the first region.
    when: The address moves into another rule’s coverage.
    then: Placement changes to the second region with one assignment version increment.
    provenance: test
    source: >-
      services-crm:modules/regions/tests/integration/services/region-assignment/region-assignment.integration.spec.js#moving
      › moves a contact from region A to region B when its address moves
edge_cases:
  - id: SC-CAP-CRM-REG-008-03
    given: A deciding-address edit stays within the current region.
    when: The coordinator submits another edit using the version returned by the first save.
    then: The unchanged placement introduces no additional version conflict.
    provenance: test
    source: >-
      services-crm:modules/regions/tests/integration/services/region-assignment/region-assignment-delivery.integration.spec.js#a
      deciding-address change that leaves the region where it was › takes the next save sent with the version
      the save answered, which is no longer stale
constraints: []
depends_on:
  - CAP-CRM-REG-003
  - CAP-CRM-REG-006
related:
  - CAP-CRM-REG-003
  - CAP-CRM-REG-006
  - CAP-CRM-REG-009
  - CAP-CRM-REG-011
journeys:
  - JNY-CRM-REG-003
  - JNY-CRM-REG-005
open_questions: []
evidence:
  - >-
    services-crm: Examined the pinned regional implementation, its declared constraints and the supporting
    records.
  - 'react-console: Examined the pinned regional configuration surfaces and their available test evidence.'
  - 'services-crm:docs/migrations/crm-regions/README.md'
  - 'services-crm:docs/milestones/CRM-M1-contacts-and-organizations.plan.md'
  - >-
    services-crm:modules/regions/tests/integration/services/region-assignment/region-assignment.integration.spec.js
  - >-
    services-crm:modules/regions/tests/integration/services/region-assignment/region-assignment-delivery.integration.spec.js
  - 'services-crm:modules/regions/services/region-assignment/region-assignment.ts'
  - 'services-crm:modules/entities/services/persistence/contacts.ts'
  - 'services-crm:modules/entities/services/affiliations/affiliation-date-reconciliation.ts'
  - >-
    services-crm:modules/regions/tests/integration/services/region-assignment/region-assignment-rules.integration.spec.js
  - 'react-console:acceptance/crm/playwright/journeys/configuration/CRM-ACC-0357.spec.ts'
controls: [TC-DAT-001, TC-DAT-002, TC-DAT-003, TC-IAM-001, TC-IAM-005, TC-IAM-006, TC-LOG-005, TC-MON-001, TC-MON-002, TC-PI-001, TC-PI-002, TC-PI-003, TC-PI-004, TC-PI-005, TC-PRV-001, TC-PRV-002, TC-TEN-001, TC-TEN-002, TC-TEN-003]
exercised_by: [FAS-CRM-REG-03-01, FAS-CRM-REG-03-02, FAS-CRM-REG-03-03, FAS-CRM-REG-03-04, FAS-CRM-REG-03-05]
suites: [TST-CRM-REG-021, TST-CRM-REG-028, TST-CRM-REG-047, TST-CRM-REG-056, TST-CRM-REG-057, TST-CRM-REG-058, TST-CRM-REG-060, TST-CRM-REG-062, TST-CRM-REG-066]
---

# Keep placements aligned with address changes

## Purpose

Rhythm updates a record’s regional placement when the address used for that placement changes.

## How It Works

A record save commits its address before regional placement runs. The subsequent change-event processing reconciles the address and policy currently stored. It remembers the deciding source and address so repeated events usually require no write. Changing another address leaves placement alone. Moving or removing the deciding address can change or clear the calculated region. Updating an organization address also reaches contacts linked to that address without copying their address rows. The assignment itself writes regional information on the contact record when needed. Overrides and update locks prevent assignment. A changed region advances the record version, while remembering an address with the same region does not. Queue redelivery can recover a failed attempt. Affiliation expiry by itself does not trigger this processing.

## Relationships

- Configure automatic placement and its address sources establishes participation and address-source choices for placement.
- Determine an address’s geographic region supplies the common geographic decision.
- Preserve a deliberate regional exception preserves deliberate record exceptions during regional processing.
- Apply the current policy across existing records applies policy changes to existing records.
