---
id: CAP-CRM-ENT-012
title: Review organization role compliance and prepare notices
product_area: crm
module: entities
revision: 1
status: approved
product_intent: confirmed
implementation_evidence: observed
source_sha:
  services-crm: 7ea48a414c42eade10d60753b8bcc323b1dd726f
  react-console: da63075876b2c5f4a6af07d457bb24ea8bf4c0c7
intent_sources:
  - >-
    services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.plan.md#1.
    Objective
  - 'services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.plan.md#A7'
  - >-
    services-crm:docs/milestones/CRM-M1-contacts-and-organizations.plan.md#9.
    Acceptance scenarios
purpose: >-
  Staff assess an organization’s required role holders and prepare communication
  about missing responsibilities.
outcomes:
  - >-
    Each organization retained to rules carries a status and the violations that
    explain it.
  - A notice moves out exactly once per fall into non-compliance.
  - >-
    An administrator can preview the notice and re-apply changed rules to the
    complete tenant.
actors:
  - ACT-CRM-001
  - ACT-CRM-003
  - ACT-CRM-004
  - ACT-CRM-005
surfaces:
  - console
  - api
  - job
preconditions: []
postconditions: []
permissions:
  - key: crm.organizations.update
    where_checked: api
    scope: tenant
  - key: crm.organization-types.update
    where_checked: api
    scope: tenant
  - key: crm.organizations.read
    where_checked: service
    scope: tenant
  - key: crm.contacts.read
    where_checked: service
    scope: tenant
rules:
  - id: BR-CAP-CRM-ENT-012-01
    proved_by: [ATS-CRM-ENT-012-01, ATS-CRM-ENT-012-24, TST-CRM-ENT-787]
    statement: >-
      Role compliance applies only through an existing organization type that
      enables it; other organizations begin without compliance data and lose
      prior status and violations on recalculation.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/organizations/compliance/organization-compliance.integration.spec.js#with
      a type that holds it to no rule, or with no type: no status and no
      violations
    origin: none
  - id: BR-CAP-CRM-ENT-012-02
    proved_by: [ATS-CRM-ENT-002-16, ATS-CRM-ENT-012-02, ATS-CRM-ENT-012-19, TST-CRM-ENT-775, TST-CRM-ENT-786]
    statement: >-
      New organizations governed by minimum-role rules start out of compliance
      with a zero-holder violation for each minimum and send no notice on
      creation.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/organizations/compliance/organization-compliance.integration.spec.js#with
      a type that holds it to rules: not in compliance, every rule with a
      minimum violated (it has no contact yet)
    origin: none
  - id: BR-CAP-CRM-ENT-012-03
    proved_by: [ATS-CRM-ENT-010-12, ATS-CRM-ENT-011-15, ATS-CRM-ENT-012-03, ATS-CRM-ENT-012-21, ATS-CRM-ENT-012-22, ATS-CRM-ENT-012-24, TST-CRM-ENT-787]
    statement: >-
      Compliance counts each contact once for each role effective on a current
      affiliation, primary or secondary, compares counts to minimum and maximum
      bounds including zero, and reports violations in rule order.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/organizations/compliance/organization-compliance.integration.spec.js#brings
      an organization into compliance for a role held on a current affiliation
      that is not primary
    origin: none
  - id: BR-CAP-CRM-ENT-012-04
    proved_by: [ATS-CRM-ENT-012-04, ATS-CRM-ENT-012-20, ATS-CRM-ENT-014-09, TST-CRM-ENT-787]
    statement: >-
      Affiliation and role changes or contact deletion recalculate affected
      organizations from before and after the change when they have rules or
      existing status; the DISABLE_CONTACT_ROLE_COMPLIANCE_CHECK notes marker
      suppresses save reactions but not deletion reactions.
    kind: lifecycle
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/organizations/compliance/compliance-change-events.integration.spec.js#a
      contact saved with the notes DISABLE_CONTACT_ROLE_COMPLIANCE_CHECK:
      nothing (its delete still records)
    origin: none
  - id: BR-CAP-CRM-ENT-012-05
    proved_by: [ATS-CRM-ENT-012-05, ATS-CRM-ENT-012-24, TST-CRM-ENT-787]
    statement: >-
      Compliance writes status, violations and version only when violations
      differ and serializes recalculation per organization, so duplicate or
      delayed events do not create redundant saves.
    kind: invariant
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/organizations/compliance/organization-compliance.integration.spec.js#writes
      nothing for a duplicate delivery, or a second run on an unchanged
      organization
    origin: none
  - id: BR-CAP-CRM-ENT-012-06
    proved_by: [ATS-CRM-ENT-012-06, ATS-CRM-ENT-012-23, ATS-CRM-ENT-012-24, ATS-CRM-ENT-018-12, TST-CRM-ENT-775, TST-CRM-ENT-786]
    statement: >-
      Changing type rules or an organization’s type leaves compliance unchanged
      until a contact reaction, date reconciliation or reprocess reaches it, and
      organization writes ignore supplied compliance status and violations.
    kind: policy
    enforced_at:
      - service
      - api
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/organization-compliance.e2e.spec.js#keep
      them against a PUT that sends others back
    origin: none
  - id: BR-CAP-CRM-ENT-012-07
    proved_by: [ATS-CRM-ENT-012-07, ATS-CRM-ENT-012-24, TST-CRM-ENT-787]
    statement: >-
      A notifying type submits one transactionally deduplicated notice when an
      organization enters not in compliance, using its written version as part
      of the key; rollback, repeated delivery, continuing noncompliance and
      return to compliance do not submit another notice.
    kind: lifecycle
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/organizations/compliance/organization-compliance.integration.spec.js#is
      not submitted again while the organization stays out of compliance, nor
      for a type that does not notify
    origin: none
  - id: BR-CAP-CRM-ENT-012-08
    proved_by: [ATS-CRM-ENT-007-08, ATS-CRM-ENT-012-08, ATS-CRM-ENT-012-17, TST-CRM-ENT-787]
    statement: >-
      Compliance recipients are current affiliates with primary e-mail addresses
      and a configured notification role, or all current affiliates when no
      notification roles are selected, with at most 500 recipients per request.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/organizations/compliance/compliance-reads.integration.spec.js#leaves
      out the contact whose addresses sit past the primary slot, no secondary
      address standing in
    origin: none
  - id: BR-CAP-CRM-ENT-012-09
    proved_by: [ATS-CRM-ENT-012-09, TST-CRM-ENT-775, TST-CRM-ENT-787]
    statement: >-
      Compliance mail uses the organization type’s Platform-owned template, and
      a missing template blocks delivery.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: documented
    source: >-
      services-crm:docs/entities-api.md#`compliance_notification_template`: the
      e-mail the type sends
    origin: none
  - id: BR-CAP-CRM-ENT-012-10
    proved_by: [ATS-CRM-ENT-012-10, ATS-CRM-ENT-012-18, TST-CRM-ENT-786, TST-CRM-ENT-787]
    statement: >-
      Organization update permission allows tenant-wide compliance reprocessing
      with one running request per tenant, resumable batches, retryable failures
      and final failure only on the last delivery with record details redacted.
    kind: lifecycle
    enforced_at:
      - api
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/compliance-reprocesses.integration.spec.js#refuses
      a second running run for the tenant
    origin: none
  - id: BR-CAP-CRM-ENT-012-11
    proved_by: [ATS-CRM-ENT-012-11, ATS-CRM-ENT-012-25, TST-CRM-ENT-786, TST-CRM-ENT-787]
    statement: >-
      Draft notice previews require organization-type update and both record
      read permissions, use an actually affiliated contact and organization,
      store and send nothing, identify sample violations when needed and return
      template problems as preview results.
    kind: permission
    enforced_at:
      - api
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/email-previews.e2e.spec.js#answers
      403 to a caller who may administer types but may not read the records the
      answer contains
    origin: none
  - id: BR-CAP-CRM-ENT-012-12
    proved_by: [ATS-CRM-ENT-012-12, TST-CRM-ENT-786]
    statement: >-
      A compliance test send uses one selected template or typed draft for one
      contact, sends through Platform to the administrator’s specified address
      and reports delivery or refusal.
    kind: policy
    enforced_at:
      - api
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/organization-compliance.e2e.spec.js#answers
      400 for a template both named and typed in, or neither
    origin: none
  - id: BR-CAP-CRM-ENT-012-13
    proved_by: [ATS-CRM-ENT-012-13, ATS-CRM-ENT-017-15, TST-CRM-ENT-786]
    statement: >-
      Organization type saves remove rules with a falsy minimum and absent
      maximum, retain maximum zero, reject unknown rule roles and require lists
      or nonempty compliance options when their corresponding restriction or
      compliance switch is enabled.
    kind: validation
    enforced_at:
      - api
      - service
    must_prove: true
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/organization-types.ts:organizationTypeProblems
    origin: none
  - id: BR-CAP-CRM-ENT-012-14
    proved_by: []
    statement: >-
      Out-of-compliance organizations appear as attention items with their
      violations and the time noncompliance began.
    kind: policy
    enforced_at:
      - service
    must_prove: false
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/organization-attention-items.queries.integration.spec.js#answers
      the organizations out of compliance by id, and no other
    origin: none
  - id: BR-CAP-CRM-ENT-012-15
    proved_by: [ATS-CRM-ENT-012-14, TST-CRM-ENT-787]
    statement: >-
      A compliance-template change merges specified fields or clears the
      template independently of other organization-type values.
    kind: lifecycle
    enforced_at:
      - service
    must_prove: true
    provenance: observed
    source: >-
      services-crm:modules/entities/services/templates/compliance-notification-template.ts:effectiveTemplateChange
    origin: none
  - id: BR-CAP-CRM-ENT-012-16
    proved_by: [ATS-CRM-ENT-012-15]
    statement: >-
      Invalid template content prevents a type change; a later template
      persistence failure compensates a committed type create by deleting it or
      restores an existing type’s content through another versioned save.
    kind: lifecycle
    enforced_at:
      - service
    must_prove: true
    provenance: observed
    source: >-
      services-crm:modules/entities/services/persistence/organization-types.ts:updateOrganizationType
    origin: none
  - id: BR-CAP-CRM-ENT-012-17
    proved_by: [ATS-CRM-ENT-012-16, TST-CRM-ENT-787]
    statement: >-
      Organization type deletion commits before template archival, and failed
      archival is logged without recreating the type.
    kind: lifecycle
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/templates/compliance-notification-template.integration.spec.js#keeps
      the delete and logs the type id when Platform cannot clear it: the type is
      gone either way
    origin: none
scenarios:
  - id: SC-CAP-CRM-ENT-012-01
    given: Bayview Guild's type requires one Billing Contact.
    when: 'Daniel joins with the Billing Contact role, then the role ends.'
    then: 'Bayview moves into compliance, then out, and the notice moves out once.'
    provenance: test
    source: >-
      react-console:acceptance/crm/playwright/journeys/organizations/CRM-ACC-0122.spec.ts#CRM-ACC-0122
  - id: SC-CAP-CRM-ENT-012-02
    given: A type's minimum is raised.
    when: The administrator reprocesses compliance.
    then: >-
      Every organization of the type is recalculated in batches and the run
      completes.
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/organizations/compliance/compliance-reprocess.integration.spec.js#answers
      the run completed once the following batches reach the end, every
      organization counted
edge_cases:
  - id: SC-CAP-CRM-ENT-012-03
    given: A compliant chapter.
    when: The administrator previews its notice.
    then: >-
      The preview uses example violations and says the violations are sample
      data.
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/email-previews/email-preview.integration.spec.js#is
      previewed with the purpose contract’s example violations, and says so in
      sample_data
constraints: []
depends_on:
  - CAP-CRM-ENT-011
  - CAP-CRM-ENT-018
related:
  - CAP-CRM-ENT-002
  - CAP-CRM-ENT-009
  - CAP-CRM-ENT-010
  - CAP-CRM-ENT-011
  - CAP-CRM-ENT-018
journeys:
  - JNY-CRM-ENT-002
open_questions: []
feature: FEAT-CRM-ENT-05
concepts:
  - CON-CRM-ENT-002
  - CON-CRM-ENT-003
evidence:
  - >-
    `services-crm` at `7ea48a414c42eade10d60753b8bcc323b1dd726f`: examined
    `services/organizations/compliance/*.ts`, `services/email-previews/email-preview.ts`,
    `services/templates/compliance-notification-template.ts`, `aws/api/organizations.routes.ts`
    (`reprocessCompliance`, `previewContactRoleComplianceEmail`),
    `aws/api/email-previews.routes.ts`, `attention/organizations-out-of-compliance.attention.ts`,
    `db/organization-compliance-violations.table.ts`, `db/compliance-reprocesses.table.ts`,
    `docs/entities-api.md` ("Organization types"); tests `e2e/organization-compliance.e2e.spec.js`,
    `e2e/email-previews.e2e.spec.js`, `e2e/organization-types.e2e.spec.js`,
    `integration/services/organizations/compliance/*.integration.spec.js`,
    `integration/services/email-previews/email-preview.integration.spec.js`,
    `integration/services/persistence/compliance-reprocesses.integration.spec.js`,
    `organization-attention-items.queries.integration.spec.js`.
  - >-
    `react-console` at `da63075876b2c5f4a6af07d457bb24ea8bf4c0c7`: examined inventory
    RUL-CRM-ORG-050 to 067; journeys CRM-ACC-0121 to 0127, 0136, 0211.
approved_by: Andrew
approved_at: 2026-10-22
controls: [TC-AVL-004, TC-AZ-001, TC-AZ-002, TC-AZ-003, TC-AZ-004, TC-AZ-006, TC-DAT-001, TC-DAT-002, TC-DAT-003, TC-IAM-001, TC-IAM-005, TC-IAM-006, TC-LOG-004, 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, TC-VEN-001, TC-VEN-003]
narrative:
  status: verified
  review: NARRATIVE-REVIEW-2026-10-23
narrative_sources:
  - section: Why It Works This Way
    provenance: inferred
    source: >-
      services-crm:modules/entities/tests/integration/services/organizations/compliance/organization-compliance.integration.spec.js#with
      a type that holds it to no rule, or with no type: no status and no
      violations;
      services-crm:modules/entities/tests/integration/services/organizations/compliance/organization-compliance.integration.spec.js#with
      a type that holds it to rules: not in compliance, every rule with a
      minimum violated (it has no contact yet);
      services-crm:modules/entities/tests/integration/services/organizations/compliance/organization-compliance.integration.spec.js#brings
      an organization into compliance for a role held on a current affiliation
      that is not primary;
      services-crm:modules/entities/tests/integration/services/organizations/compliance/compliance-change-events.integration.spec.js#a
      contact saved with the notes DISABLE_CONTACT_ROLE_COMPLIANCE_CHECK:
      nothing (its delete still records);
      services-crm:modules/entities/tests/integration/services/organizations/compliance/organization-compliance.integration.spec.js#writes
      nothing for a duplicate delivery, or a second run on an unchanged
      organization;
      services-crm:modules/entities/tests/e2e/organization-compliance.e2e.spec.js#keep
      them against a PUT that sends others back;
      services-crm:modules/entities/tests/integration/services/organizations/compliance/organization-compliance.integration.spec.js#is
      not submitted again while the organization stays out of compliance, nor
      for a type that does not notify;
      services-crm:modules/entities/tests/integration/services/organizations/compliance/compliance-reads.integration.spec.js#leaves
      out the contact whose addresses sit past the primary slot, no secondary
      address standing in;
      services-crm:docs/entities-api.md#`compliance_notification_template`: the
      e-mail the type sends;
      services-crm:modules/entities/tests/integration/services/persistence/compliance-reprocesses.integration.spec.js#refuses
      a second running run for the tenant;
      services-crm:modules/entities/tests/e2e/email-previews.e2e.spec.js#answers
      403 to a caller who may administer types but may not read the records the
      answer contains;
      services-crm:modules/entities/tests/e2e/organization-compliance.e2e.spec.js#answers
      400 for a template both named and typed in, or neither;
      services-crm:modules/entities/services/persistence/organization-types.ts:organizationTypeProblems;
      services-crm:modules/entities/tests/integration/services/persistence/organization-attention-items.queries.integration.spec.js#answers
      the organizations out of compliance by id, and no other;
      services-crm:modules/entities/services/templates/compliance-notification-template.ts:effectiveTemplateChange;
      services-crm:modules/entities/services/persistence/organization-types.ts:updateOrganizationType;
      services-crm:modules/entities/tests/integration/services/templates/compliance-notification-template.integration.spec.js#keeps
      the delete and logs the type id when Platform cannot clear it: the type is
      gone either way
    note: >-
      Operational rationale inferred from the cited behavior; no personal motive
      or historical decision is asserted.
repositories:
  - repo: services-crm
    role: owner
    paths:
      - modules/entities/
      - docs/entities-api.md
      - modules/entities/services/persistence/organization-types.ts
      - modules/entities/services/templates/compliance-notification-template.ts
      - modules/entities/tests/e2e/email-previews.e2e.spec.js
      - modules/entities/tests/e2e/organization-compliance.e2e.spec.js
      - >-
        modules/entities/tests/integration/services/email-previews/email-preview.integration.spec.js
      - >-
        modules/entities/tests/integration/services/organizations/compliance/compliance-change-events.integration.spec.js
      - >-
        modules/entities/tests/integration/services/organizations/compliance/compliance-reads.integration.spec.js
      - >-
        modules/entities/tests/integration/services/organizations/compliance/compliance-reprocess.integration.spec.js
      - >-
        modules/entities/tests/integration/services/organizations/compliance/organization-compliance.integration.spec.js
      - >-
        modules/entities/tests/integration/services/persistence/compliance-reprocesses.integration.spec.js
      - >-
        modules/entities/tests/integration/services/persistence/organization-attention-items.queries.integration.spec.js
      - >-
        modules/entities/tests/integration/services/templates/compliance-notification-template.integration.spec.js
  - repo: react-console
    role: console
    paths:
      - 'app/(console)/console/[tenantId]/crm/contacts/'
      - acceptance/crm/playwright/journeys/organizations/CRM-ACC-0122.spec.ts
exercised_by: [FAS-CRM-ENT-05-01, FAS-CRM-ENT-05-02, FAS-CRM-ENT-05-03, FAS-CRM-ENT-05-04, FAS-CRM-ENT-05-05]
suites: [TST-CRM-ENT-775, TST-CRM-ENT-786, TST-CRM-ENT-787]
---

# Review organization role compliance and prepare notices

## Purpose

Staff need to see whether an organization has the representatives its configured policy requires and to respond when coverage falls short. This capability calculates effective contact-role coverage, preserves explanatory violations, and coordinates notice preparation and reprocessing without treating a manually supplied status as an authoritative assessment.

## How It Works

The organization's type enables compliance and provides role-count bounds. Recalculation counts distinct contacts holding each role on current affiliations, including secondary ties, and reports violations in policy order. New organizations with minimum requirements begin with missing-holder violations but do not send a creation notice. Organizations without an applicable policy have no assessment after recalculation.

Contact relationship and role changes, deletion, and date boundaries can trigger reevaluation of affected organizations. Calculations serialize per organization and write only a changed violation result. A type-policy or organization-type change alone does not immediately refresh stored assessments; reaction, reconciliation, or explicit reprocessing must reach the organization. Clients cannot replace calculated status by supplying their own values.

For notifying types, entry into noncompliance submits a deduplicated notice alongside the assessment. Continuing noncompliance and repeated delivery do not submit another transition notice. Recipients come from current affiliates with primary e-mail addresses and the configured notification roles, or all qualifying affiliates when none are selected. The request has a recipient limit, and Platform's missing template prevents delivery.

Administrators can preview drafts without sending or storing them, make a test send to a specified address, and run resumable workspace reprocessing. One running request per workspace prevents overlapping scans. Type-template changes cross a service boundary: invalid content refuses the change, later persistence failure uses forward compensation, and template cleanup failure after deletion is logged without recreating the type.

## Example

A chapter type requires a billing contact. Its current billing representative's role ends, and reevaluation records the missing holder. If notification is enabled, the transition submits the configured notice for eligible recipients. Repeated event delivery leaves the same assessment and submission intact. An administrator then changes the policy and requests reprocessing, which revisits existing chapters rather than assuming the catalogue save already recalculated them.

At Harbor West Chapter, Ruth Kim holds Officer through two current affiliations and Lena Park through one. With a minimum of two Officers, the two people satisfy coverage; Ruth's extra relationship is not a third holder. After the Billing Contact period ends on October 23, staff inspect the reassessed shortage separately from the notice request. The administrator uses draft preview to inspect wording and a test send to her own address to inspect the delivery path. Neither proves delivery to chapter representatives. After changing a policy, she inspects the resumable reprocess outcome before relying on every chapter's stored assessment as current.

## Why It Works This Way

Our reading is that distinct effective people are the relevant coverage measure: multiple ties must not inflate a firm's count, and a secondary representative can still perform the responsibility. Server-owned assessments prevent a record edit from claiming compliance without the underlying holders. Serialization and changed-result writes protect versions and history from duplicate events.

Transition-based notices prevent continuing gaps from producing a message on every save. Transactional submission deduplication keeps rollback and retry from separating the notice's cause from its assessment, but does not prove final delivery. Preview authorization protects the contact and organization information included in a draft. Durable batched reprocessing supports policy repair across a workspace without assuming an immediate global calculation. Forward template compensation acknowledges that a Platform call is not the same database transaction as the type save, and therefore is not an invisible rollback.

## Relationships

Organization Type supplies compliance options and allowed-role policy. Affiliations and Roles supplies effective responsibilities; date reconciliation supplies boundary reevaluation. Contact changes feed reactions through committed events. Platform notifications owns templates and sending, reached through its service contract. Enabled organization attention-item publication exposes stored violations, while record history captures changed assessments with system attribution.
