---
id: CAP-CRM-DQ-003
title: Set the response to duplicate matches
feature: FEAT-CRM-DQ-01
product_area: crm
module: data-quality
revision: 1
status: draft
product_intent: confirmed
implementation_evidence: observed
supersedes:
  - CAP-CRM-MRG-004
source_sha:
  services-crm: a8d1364f1dd44b723bb5f9acba12f983e9ec8d69
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
intent_sources:
  - 'services-crm:docs/milestones/CRM-M4-duplicates-and-merge.md#D-32'
  - 'services-crm:docs/merging/README.md#Status (architecture and plan approved by Andrew 2026-10-11)'
purpose: Administrators choose how the workspace responds to particular duplicate evidence.
outcomes:
  - Workspace configuration selects creation responses independently of scoring weights.
  - Candidate review remains available under every configured response.
concepts:
  - CON-CRM-DQ-001
actors:
  - ACT-CRM-009
  - ACT-CRM-005
surfaces:
  - console
  - api
permissions:
  - key: crm.duplicates.configure
    where_checked: api
    scope: tenant
rules:
  - id: BR-CAP-CRM-DQ-003-01
    proved_by: [ATS-CRM-DQ-003-01, ATS-CRM-DQ-003-09, ATS-CRM-DQ-003-12, TST-CRM-DQ-176]
    statement: >-
      For the relevant record type, active rules are evaluated by position and the first matching evidence
      pattern whose confidence floor is met determines the action.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/unit/services/detection/duplicate-rule-actions.spec.js#is decided
      by the first matching rule in position order, not by a later one that also matches
    origin: 'services-crm:docs/merging/README.md#Status (architecture and plan approved by Andrew 2026-10-11)'
  - id: BR-CAP-CRM-DQ-003-02
    proved_by: [ATS-CRM-DQ-003-02, ATS-CRM-DQ-003-09, ATS-CRM-DQ-003-12, TST-CRM-DQ-176, TST-CRM-DQ-180]
    statement: >-
      Review, warn and block all retain candidates for review; warn also informs creation checks and block
      refuses ordinary creation without hiding or retiring the pair.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/unit/services/detection/duplicate-rule-actions.spec.js#stores the
      candidate for a block rule too, the block acting at the create and never on the queue (D-32)
    origin: 'services-crm:docs/milestones/CRM-M4-duplicates-and-merge.md#D-32'
  - id: BR-CAP-CRM-DQ-003-03
    proved_by: [ATS-CRM-DQ-003-03, ATS-CRM-DQ-003-12]
    statement: >-
      When no rule qualifies, including when none exists for the record type, the pair remains available for
      review.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/unit/services/detection/duplicate-rule-actions.spec.js#keeps a
      pair no rule matches for review, which is detection’s default
    origin: none
  - id: BR-CAP-CRM-DQ-003-04
    proved_by: [ATS-CRM-DQ-003-04, ATS-CRM-DQ-003-12]
    statement: >-
      E-mail rules require shared e-mail evidence; name-and-postal rules require both name agreement and place
      agreement; name-and-phone rules require name agreement and a shared number; organization-name-and-domain
      rules require both kinds of agreement.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/unit/services/detection/duplicate-rule-actions.spec.js#does not
      match on name and postal code when a name DISAGREES, however the places compare
    origin: none
  - id: BR-CAP-CRM-DQ-003-05
    proved_by: [ATS-CRM-DQ-003-05, ATS-CRM-DQ-003-12]
    statement: >-
      The minimum band sets the score floor, with medium used when the configured band is absent or
      unrecognized.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/unit/services/detection/duplicate-rule-actions.spec.js#holds a
      rule naming no band to the default band, which is what the column defaults to
    origin: none
  - id: BR-CAP-CRM-DQ-003-06
    proved_by: [ATS-CRM-DQ-003-06, ATS-CRM-DQ-003-12, TST-CRM-DQ-180, TST-CRM-DQ-182]
    statement: >-
      Rule replacement requires the current version and matching identity and rejects stale or missing
      versions, invalid actions and locked rules.
    kind: validation
    enforced_at:
      - api
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/e2e/duplicates.e2e.spec.js#refuses the version that has moved on
      (409)
    origin: none
  - id: BR-CAP-CRM-DQ-003-07
    proved_by: [ATS-CRM-DQ-003-07, ATS-CRM-DQ-003-12, TST-CRM-DQ-180]
    statement: 'Each workspace rule requires a distinct integer position from 1 to 100 and a trimmed, nonempty name.'
    kind: validation
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/integration/services/persistence/duplicate-rules.integration.spec.js#refuses
      the place another rule already holds
    origin: none
  - id: BR-CAP-CRM-DQ-003-08
    proved_by: [ATS-CRM-DQ-003-11, ATS-CRM-DQ-003-12]
    statement: >-
      Workspace setup supplies Contact Email Match with block and the portal switch, Contact Name and ZIP with
      warn, and Organization Website Match with review; repeat setup preserves edits, and the API offers
      neither rule creation nor deletion.
    kind: lifecycle
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/integration/services/persistence/duplicate-rules.integration.spec.js#leaves
      the tenant’s own name, switch and action exactly as the tenant left them
    origin: none
  - id: BR-CAP-CRM-DQ-003-09
    proved_by: [ATS-CRM-DQ-003-08, ATS-CRM-DQ-003-12, TST-CRM-DQ-182]
    statement: >-
      Only duplicate-configuration permission holders may read or update rules; duplicate readers and
      reviewers without that permission are refused.
    kind: permission
    enforced_at:
      - api
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/e2e/duplicates.e2e.spec.js#answers 403 to a reviewer reading the
      rules, which are the tenant’s configuration
    origin: 'services-crm:docs/merging/architecture.md#9. Security and tenant isolation'
    note: >-
      The bound test proves service-route permission refusal. The configuration-page guard lies outside the
      registered source scope and remains an examination finding.
  - id: BR-CAP-CRM-DQ-003-10
    proved_by: []
    statement: >-
      The saved portal switch does not influence portal sign-up matching, which does not read workspace
      duplicate rules.
    kind: policy
    enforced_at:
      - service
    must_prove: false
    provenance: documented
    source: 'services-crm:docs/merging/duplicate-detection.md#15. Portal sign-up (the `portal_sign_up` origin)'
    origin: none
  - id: BR-CAP-CRM-DQ-003-11
    proved_by: []
    statement: >-
      Updating a rule leaves existing candidates unevaluated until subsequent record changes or a re-check
      applies the new settings.
    kind: policy
    enforced_at:
      - service
    must_prove: false
    provenance: observed
    source: 'services-crm:modules/duplicates/services/persistence/duplicate-rules.ts:updateDuplicateRule'
    origin: none
  - id: BR-CAP-CRM-DQ-003-12
    proved_by: []
    statement: Workspace data exports include duplicate-rule configuration.
    kind: policy
    enforced_at:
      - service
    must_prove: false
    provenance: observed
    source: 'services-crm:modules/duplicates/export/duplicate-rules.dataset.ts'
    origin: none
scenarios:
  - id: SC-CAP-CRM-DQ-003-01
    given: 'Two e-mail rules qualify, with warning placed ahead of the seeded blocker.'
    when: Staff create a contact sharing an existing e-mail.
    then: Creation proceeds with warning and a review pair; restoring blocker precedence refuses another create.
    provenance: test
    source: 'react-console:acceptance/crm/playwright/journeys/merging/CRM-ACC-0244.spec.ts#CRM-ACC-0244'
  - id: SC-CAP-CRM-DQ-003-02
    given: An existing workspace pair qualifies under a blocker.
    when: Detection scores it again.
    then: The pair remains open and reviewable under its original identity.
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/integration/services/detection/duplicate-detection.integration.spec.js#keeps
      the same pair after the re-score, by its id
edge_cases: []
constraints: []
depends_on:
  - CAP-CRM-DQ-002
related:
  - CAP-CRM-DQ-004
  - CAP-CRM-DQ-005
journeys:
  - JNY-CRM-DQ-005
open_questions: []
evidence:
  - repository: services-crm
    source_sha: a8d1364f1dd44b723bb5f9acba12f983e9ec8d69
    paths:
      - docs/merging/README.md
      - docs/merging/architecture.md
      - docs/merging/duplicate-detection.md
      - docs/milestones/CRM-M4-duplicates-and-merge.md
      - modules/duplicates/db/duplicate-rules.table.ts
      - modules/duplicates/export/duplicate-rules.dataset.ts
      - modules/duplicates/services/detection/duplicate-rule-actions.ts
      - modules/duplicates/services/persistence/duplicate-rules.ts
      - modules/duplicates/tests/e2e/duplicates.e2e.spec.js
      - modules/duplicates/tests/integration/services/detection/duplicate-detection.integration.spec.js
      - modules/duplicates/tests/integration/services/persistence/duplicate-rules.integration.spec.js
      - modules/duplicates/tests/unit/services/detection/duplicate-rule-actions.spec.js
    note: 'Static examination connects rule claims to implementation, declared checks and the cited tests.'
  - repository: react-console
    source_sha: baf6dae382d6f41dce976c7f848667ccc37e076f
    paths:
      - acceptance/crm/playwright/journeys/merging/CRM-ACC-0244.spec.ts
    note: 'Static examination connects rule claims to implementation, declared checks and the cited tests.'
preconditions: []
postconditions: []
controls: [TC-AZ-001, TC-AZ-002, TC-AZ-003, TC-AZ-004, TC-AZ-005, TC-AZ-006, TC-DAT-001, TC-DAT-002, TC-DAT-003, TC-LOG-004, TC-LOG-005, TC-PI-001, TC-PI-003, TC-PRV-001, TC-PRV-002, TC-TEN-001, TC-TEN-002, TC-TEN-003]
exercised_by: [FAS-CRM-DQ-01-02, FAS-CRM-DQ-01-04]
suites: [TST-CRM-DQ-176, TST-CRM-DQ-180, TST-CRM-DQ-182]
---

# Set the response to duplicate matches

## Purpose

Administrators choose how the workspace responds to particular duplicate evidence.

## How It Works

An administrator opens duplicate configuration and edits an existing rule. Rules apply to one record type and are considered in position order. The first qualifying rule chooses an action for the pair. All actions retain the pair for staff review. Warning and blocking also affect checks made during creation. A minimum confidence band restricts when the rule qualifies. Saving replaces the complete rule and requires the version previously read. A stale edit or a locked rule is refused. Changing configuration does not refresh existing evidence immediately; record changes and re-checks perform that work. The portal switch is retained as configuration but does not currently control portal matching.

## Relationships

Detection applies these settings to stored candidates. Draft checks interpret their warning and blocking actions. Re-checks refresh existing pairs after configuration changes.
