---
id: CAP-CRM-DQ-004
title: Check drafts and account for block overrides
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-005
  - CAP-CRM-MRG-006
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/experience/functional-requirements.md#CON-6 (package approved by Andrew
    2026-10-13)
purpose: 'Staff can inspect possible matches before creation, and authorized exceptions retain their reasons.'
outcomes:
  - Draft matching reveals likely counterparts without retaining form checks.
  - 'Authorized blocking exceptions retain their author, time and explanation.'
concepts:
  - CON-CRM-DQ-001
actors:
  - ACT-CRM-001
  - ACT-CRM-009
  - ACT-CRM-004
surfaces:
  - console
  - api
permissions:
  - key: crm.duplicates.read
    where_checked: api
    scope: tenant
  - key: crm.contacts.create-despite-duplicate-block
    where_checked: service
    scope: tenant
  - key: crm.organizations.create-despite-duplicate-block
    where_checked: service
    scope: tenant
rules:
  - id: BR-CAP-CRM-DQ-004-01
    proved_by: [ATS-CRM-DQ-004-01, ATS-CRM-DQ-004-07, ATS-CRM-DQ-004-12, TST-CRM-DQ-176, TST-CRM-DQ-180]
    statement: >-
      Draft checks use detection's keys and scoring within the workspace and exclude the edited record from
      its own results.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/integration/services/detection/duplicate-checks.integration.spec.js#does
      not answer the record itself, a record never being its own duplicate
    origin: 'services-crm:docs/merging/experience/functional-requirements.md#CON-6'
  - id: BR-CAP-CRM-DQ-004-02
    proved_by: [ATS-CRM-DQ-004-01, ATS-CRM-DQ-004-12, TST-CRM-DQ-180]
    statement: >-
      A check returns at most five matches in descending score order, using the weakest active-rule confidence
      floor for that type or medium when there are no active rules.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/integration/services/detection/duplicate-checks.integration.spec.js#answers
      at most the five a form shows, however many the tenant holds
    origin: none
  - id: BR-CAP-CRM-DQ-004-03
    proved_by: []
    statement: >-
      Each result supplies record number, name, score, confidence and reasons, and the response indicates
      whether a blocking rule applies.
    kind: policy
    enforced_at:
      - api
      - service
    must_prove: false
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/e2e/duplicates.e2e.spec.js#says the tenant’s block rule fires on
      it, which is what makes the create refuse
    origin: none
  - id: BR-CAP-CRM-DQ-004-04
    proved_by: [ATS-CRM-DQ-004-02, ATS-CRM-DQ-004-07, ATS-CRM-DQ-004-12, TST-CRM-DQ-176, TST-CRM-DQ-182]
    statement: 'Checking a draft writes no record, match key or review candidate.'
    kind: invariant
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/e2e/duplicates.e2e.spec.js#writes nothing: the draft is not a
      record, so the queue is as it was
    origin: none
  - id: BR-CAP-CRM-DQ-004-05
    proved_by: [ATS-CRM-DQ-004-03, ATS-CRM-DQ-004-12, TST-CRM-DQ-182]
    statement: 'The duplicate-read permission is required for a draft check, and callers lacking it 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 caller with no
      duplicates permission at all
    origin: 'services-crm:docs/merging/architecture.md#9. Security and tenant isolation'
  - id: BR-CAP-CRM-DQ-004-06
    proved_by: [ATS-CRM-DQ-004-04, ATS-CRM-DQ-004-12, TST-CRM-DQ-180]
    statement: >-
      An overridden create and its override record share a transaction; the record retains the author, time,
      trimmed reason, distinct blocking rules and bypassed matches, and an audit-write failure rolls back
      creation.
    kind: invariant
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/integration/services/persistence/duplicate-block-overrides.integration.spec.js#leaves
      no contact behind, the record and its audit sharing one transaction
    origin: 'services-crm:docs/milestones/CRM-M4-duplicates-and-merge.md#D-32'
  - id: BR-CAP-CRM-DQ-004-07
    proved_by: [ATS-CRM-DQ-004-04, ATS-CRM-DQ-004-05, ATS-CRM-DQ-004-12, TST-CRM-DQ-180]
    statement: >-
      An override requires a nonempty reason and at least one rule, with at most one override record for a
      created record.
    kind: validation
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/integration/services/persistence/duplicate-block-overrides.integration.spec.js#refuses
      an override naming no rule, naming the field
    origin: 'services-crm:docs/milestones/CRM-M4-duplicates-and-merge.md#D-32'
  - id: BR-CAP-CRM-DQ-004-08
    proved_by: [ATS-CRM-DQ-004-05, ATS-CRM-DQ-004-12, TST-CRM-DQ-180]
    statement: Application updates cannot alter an existing override record.
    kind: invariant
    enforced_at:
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/integration/services/persistence/duplicate-block-overrides.integration.spec.js#is
      not rewritable by the application, which holds no UPDATE on the audit
    origin: 'services-crm:docs/milestones/CRM-M4-duplicates-and-merge.md#D-32'
  - id: BR-CAP-CRM-DQ-004-09
    proved_by: []
    statement: >-
      The created record does not store override confirmation or explanation; the separate override record
      stores those details.
    kind: policy
    enforced_at:
      - service
    must_prove: false
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/e2e/duplicates.e2e.spec.js#stores neither the confirmation nor the
      reason on the record, both being about the create and not fields
    origin: none
  - id: BR-CAP-CRM-DQ-004-10
    proved_by: [ATS-CRM-DQ-004-06, ATS-CRM-DQ-004-12, TST-CRM-DQ-182]
    statement: >-
      Organization creation uses organization matching settings and requires the organization-specific
      override permission.
    kind: policy
    enforced_at:
      - api
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/e2e/organization-block-rules.e2e.spec.js#audits the override with
      its reason, its rule and the organization it was let past
    origin: 'services-crm:docs/milestones/CRM-M4-duplicates-and-merge.md#D-32'
  - id: BR-CAP-CRM-DQ-004-11
    proved_by: [ATS-CRM-DQ-004-11, ATS-CRM-DQ-004-12]
    statement: >-
      The console presents Create Anyway with a required explanation only when the refusal reports override
      authority; service checks decide whether creation proceeds.
    kind: permission
    enforced_at:
      - console
    must_prove: true
    provenance: observed
    source: 'react-console:components/records/create/duplicate-block-refusal.tsx:onCreateAnyway'
    origin: 'services-crm:docs/milestones/CRM-M4-duplicates-and-merge.md#D-32'
  - id: BR-CAP-CRM-DQ-004-12
    proved_by: []
    statement: Workspace data exports include duplicate-block override records.
    kind: policy
    enforced_at:
      - service
    must_prove: false
    provenance: observed
    source: 'services-crm:modules/duplicates/export/duplicate-block-overrides.dataset.ts'
    origin: none
  - id: BR-CAP-CRM-DQ-004-13
    proved_by: [ATS-CRM-DQ-004-08, ATS-CRM-DQ-004-12, TST-CRM-DQ-182]
    statement: A draft check rejects record types other than contacts or organizations as an invalid request.
    kind: validation
    enforced_at:
      - api
    must_prove: true
    provenance: observed
    source: 'services-crm:modules/duplicates/aws/api/duplicates.routes.ts:objectTypeOf'
    origin: none
scenarios:
  - id: SC-CAP-CRM-DQ-004-01
    given: A coordinator enters a new contact resembling an existing member's name and postal code.
    when: The form requests matching suggestions.
    then: The member appears with score and reasons without a write.
    provenance: test
    source: 'react-console:acceptance/crm/playwright/journeys/merging/CRM-ACC-0243.spec.ts#CRM-ACC-0243'
  - id: SC-CAP-CRM-DQ-004-02
    given: An authorized administrator encounters a blocked create.
    when: They choose an exception and supply its explanation.
    then: 'The contact and override retain administrator, reason, rule and bypassed match.'
    provenance: test
    source: >-
      services-crm:modules/duplicates/tests/e2e/duplicates.e2e.spec.js#audits the override with its reason and
      the record it was let past
edge_cases: []
constraints: []
depends_on:
  - CAP-CRM-DQ-003
related:
  - CAP-CRM-ENT-001
  - CAP-CRM-ENT-002
journeys:
  - JNY-CRM-DQ-005
open_questions: []
evidence:
  - repository: services-crm
    source_sha: a8d1364f1dd44b723bb5f9acba12f983e9ec8d69
    paths:
      - docs/merging/architecture.md
      - docs/merging/experience/functional-requirements.md
      - docs/milestones/CRM-M4-duplicates-and-merge.md
      - modules/duplicates/aws/api/duplicates.routes.ts
      - modules/duplicates/db/duplicate-block-overrides.table.ts
      - modules/duplicates/export/duplicate-block-overrides.dataset.ts
      - modules/duplicates/services/detection/duplicate-checks.ts
      - modules/duplicates/tests/e2e/duplicates.e2e.spec.js
      - modules/duplicates/tests/e2e/organization-block-rules.e2e.spec.js
      - modules/duplicates/tests/integration/services/detection/duplicate-checks.integration.spec.js
      - >-
        modules/duplicates/tests/integration/services/persistence/duplicate-block-overrides.integration.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-0243.spec.ts
      - components/records/create/duplicate-block-refusal.tsx
    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-CRY-004, TC-DAT-001, TC-DAT-002, TC-DAT-003, TC-LOG-001, TC-LOG-002, TC-LOG-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]
suites: [TST-CRM-DQ-176, TST-CRM-DQ-180, TST-CRM-DQ-182]
---

# Check drafts and account for block overrides

## Purpose

Staff can inspect possible matches before creation, and authorized exceptions retain their reasons.

## How It Works

A draft check compares the entered details with existing records using ordinary matching. The response contains a small ranked set of suggestions. It includes enough identity information and reasons for staff to recognize each match. No candidate is stored merely because a form was checked. Blocking settings can refuse creation. An authorized person can choose Create Anyway and supply a reason. The override records that person, the blocking rules and the existing records involved. The create and its override record commit together, so an audit failure also prevents creation. The exception details remain separate from the new contact or organization. The service owns authorization and refusal; the console presents the available action.

## Relationships

Duplicate settings determine blocking. Entities owns record creation and its refusal. This capability owns the check response and the override record.
