---
id: STATE-CRM-REG-002
title: Assignment policy switch and currency
object: DOM-CRM-REG-002
product_area: crm
module: regions
revision: 2
status: draft
design_intent: confirmed
origin: >-
  Andrew, 2026-10-08 (saving rules changes no record; staff press Recalculate) and 2026-10-11 (rule or setting
  changes move no record; manual Recalculate with a recalculation-needed banner), cited in
  services-crm:docs/migrations/crm-regions/README.md#2026-10-12 — Final acceptance (second pass)
implementation_evidence: observed
source_sha:
  services-crm: a8d1364f1dd44b723bb5f9acba12f983e9ec8d69
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
states:
  - name: never saved
    meaning: No settings row exists; reads return disabled assignment and empty coverage.
  - name: 'off'
    meaning: >-
      Persisted assignment is disabled. Rules remain available to backend preview, but placement and the
      outstanding-work flag are off.
  - name: on and current
    meaning: >-
      The highest completed-run generation covers the enabled policy’s generation; exceptions and skipped
      records may retain other placements.
  - name: on and needing recalculation
    meaning: 'The enabled policy generation exceeds completed-run coverage, or no run covers it yet.'
transitions:
  - from: never saved
    to: 'off'
    trigger: Save disabled settings or add an individual rule before settings exist.
    guard: Update authority and valid policy inputs are required.
    side_effects:
      - First-rule creation inserts disabled defaults before storing the rule.
      - The initial settings save records generation one.
    who: 'An administrator, or an API client'
    provenance: observed
    source: >-
      services-crm:modules/regions/services/persistence/region-assignment-settings.ts:lockSettingsRowForRuleWrite
  - from: never saved
    to: on and needing recalculation
    trigger: Persist the first enabled settings.
    guard: Update authority and valid settings are required; a supplied first-save version must be zero.
    side_effects:
      - Persistence creates generation one and records the change time.
      - Placement records are left untouched.
    who: 'An administrator in the assignment rules editor, or an API client'
    provenance: observed
    source: >-
      services-crm:modules/regions/services/persistence/region-assignment-settings.ts:saveRegionAssignmentSettings
  - from: 'off'
    to: on and needing recalculation
    trigger: Enable an existing disabled policy.
    guard: 'The caller needs update authority, valid settings and a matching version when supplied.'
    side_effects:
      - The save increments generation and stamps the change time.
      - Existing placements remain as stored.
    who: 'An administrator, or an API client'
    provenance: test
    source: >-
      services-crm:modules/regions/tests/integration/services/region-assignment/region-assignment-rules.integration.spec.js#region
      assignment rules and slots (integration) › whether a recalculation is needed › says a recalculation is
      needed once the rules have been saved and nothing has recalculated
  - from: on and current
    to: on and needing recalculation
    trigger: 'Accept a settings or rule write, reorder coverage, or retire an enabled target.'
    guard: The operation needs update authority and its applicable version precondition.
    side_effects:
      - The same transaction increments generation and updates change time.
      - Settings and rule writes also increment settings version.
      - No regional placement is written.
    who: 'An administrator, an API client or a regional import'
    provenance: observed
    source: >-
      services-crm:modules/regions/tests/integration/services/region-assignment/region-assignment-rules.integration.spec.js#region
      assignment rules and slots (integration) › whether a recalculation is needed › says one is needed again
      as soon as a rule is added;
      services-crm:modules/regions/services/persistence/region-assignment-settings.ts:saveRegionAssignmentSettings,
      stampRulesChanged, noteRuleRegionDeactivated
  - from: on and needing recalculation
    to: on and current
    trigger: Complete a run whose starting generation covers the current policy.
    guard: No later policy generation may remain uncovered by the completed run.
    side_effects:
      - >-
        The outstanding-work flag clears for the completed generation; protected and skipped records can
        retain other values.
    who: The recalculation run
    provenance: test
    source: >-
      services-crm:modules/regions/tests/integration/services/region-assignment/region-assignment-rules.integration.spec.js#region
      assignment rules and slots (integration) › whether a recalculation is needed › says none is needed once
      a recalculation has succeeded against those rules
  - from: on and needing recalculation
    to: 'off'
    trigger: Disable a policy with outstanding recalculation.
    guard: Update permission and any supplied version must pass.
    side_effects:
      - Generation advances while disabled assignment suppresses the outstanding-work flag.
    who: 'An administrator, or an API client'
    provenance: test
    source: >-
      services-crm:modules/regions/tests/integration/services/region-assignment/region-assignment.integration.spec.js#region
      assignment (integration) › assignment switched off › assigns nothing while assignment is off
  - from: on and current
    to: 'off'
    trigger: Disable a policy whose generation was covered.
    guard: The settings write must pass authority and any supplied version checks.
    side_effects:
      - Future automatic placement stops without clearing existing regional values.
    who: 'An administrator, or an API client'
    provenance: observed
    source: 'services-crm:modules/regions/services/persistence/region-assignment-settings.ts:recalculationNeeded'
  - from: 'off'
    to: 'off'
    trigger: Save settings or rules while keeping assignment disabled.
    guard: Update permission and relevant version checks still apply while assignment is disabled.
    side_effects:
      - Generation still advances; enabling assignment later requires a covering run.
    who: 'An administrator, or an API client'
    provenance: observed
    source: 'services-crm:modules/regions/services/persistence/region-assignment-settings.ts:stampRulesChanged'
invalid:
  - from: 'off'
    to: on and current
    why: Enabling the switch increments generation and cannot itself provide completed-run coverage.
  - from: on and needing recalculation
    to: on and current
    why: A settings save cannot finish recalculation; completion under an older generation leaves the flag set.
  - from: 'off'
    to: never saved
    why: >-
      Ordinary policy operations provide no deletion back to unsaved defaults; lifecycle cleanup can remove
      settings.
concurrency: >-
  Existing-row writers serialize their changes and increment generation transactionally. Generation comparison
  avoids clock-order ambiguity. Settings saves may omit version checks, and first saves can collide because an
  absent row has no lock. Reads of settings and rules do not request the available shared lock.
invariants:
  - INV-DOM-CRM-REG-002-01
  - INV-DOM-CRM-REG-002-07
  - INV-DOM-CRM-REG-002-08
  - INV-DOM-CRM-REG-002-09
evidence:
  - >-
    services-crm at a8d1364f: modules/regions/services/persistence/region-assignment-settings.ts
    (saveRegionAssignmentSettings, lockSettingsRowForRuleWrite, stampRulesChanged, noteRuleRegionDeactivated,
    recalculationNeeded); services/persistence/region-recalculations.ts
    (lastCompletedRecalculationRulesGeneration);
    tests/integration/services/region-assignment/region-assignment-rules.integration.spec.js.
  - >-
    react-console at baf6dae3: components/regions/recalculation.tsx (recalculatePlacement);
    components/regions/tabs.ts.
---

# Assignment policy switch and currency

## The Lifecycle

```mermaid
stateDiagram-v2
  state "never saved" as unsaved
  state "off" as disabled
  state "on and current" as current
  state "on and needing recalculation" as outstanding
  [*] --> unsaved
  unsaved --> disabled: save disabled settings or first rule
  unsaved --> outstanding: save enabled settings
  disabled --> outstanding: enable
  current --> outstanding: policy change
  outstanding --> outstanding: further policy change
  outstanding --> current: covering run completes
  outstanding --> disabled: disable
  current --> disabled: disable
  disabled --> disabled: edit while disabled
```

## Transitions Explained

These are derived policy conditions rather than stored status values. Before the first save, assignment is disabled. Accepted policy changes raise generation. With assignment enabled, a completed recalculation must cover that generation before the read reports current. Completion describes traversal coverage, including records that processing skipped.

## What Cannot Happen

Saving coverage never updates placements. Enabling assignment does not apply it to every existing record, and a run beginning before a later change cannot clear that change’s outstanding-work flag. Disabled assignment suppresses the flag but does not erase stored regions.

## Concurrency and Timing

The counter records change order without comparing clocks. Existing policy writes serialize, while initialization and unlocked multi-statement reads retain concurrency gaps. A settings write without a supplied version can overwrite newer settings.
