---
id: CAP-CRM-ENT-010
title: Keep dated relationships current
product_area: crm
module: entities
revision: 1
status: approved
product_intent: confirmed
implementation_evidence: partially-observed
source_sha:
  services-crm: 7ea48a414c42eade10d60753b8bcc323b1dd726f
  react-console: da63075876b2c5f4a6af07d457bb24ea8bf4c0c7
intent_sources:
  - 'services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.md#D-10'
  - 'services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.md#D-12'
  - 'services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.plan.md#A2'
  - 'services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.plan.md#A3'
  - 'services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.plan.md#A6'
  - 'services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.plan.md#A7'
purpose: >-
  Rhythm follows the workspace calendar when relationships start or stop taking
  effect.
outcomes:
  - >-
    Upcoming, Current and Past never overlap, and counts, rows and the service
    agree.
  - >-
    A job starting next month becomes current, with its roles and compliance, on
    its start day.
actors:
  - ACT-CRM-001
  - ACT-CRM-003
  - ACT-CRM-004
  - ACT-CRM-005
surfaces:
  - console
  - api
  - job
preconditions: []
postconditions: []
permissions:
  - key: crm.contacts.bulk
    where_checked: api
    scope: tenant
  - key: crm.contacts.read
    where_checked: api
    scope: tenant
rules:
  - id: BR-CAP-CRM-ENT-010-01
    proved_by: [ATS-CRM-ENT-010-01, TST-CRM-ENT-790]
    statement: >-
      Affiliation classification, filters, primary starts, Organization command
      dates, compliance and automatic roles use the workspace’s local business
      date rather than the server or browser date.
    kind: policy
    enforced_at:
      - service
      - console
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.business-dates.integration.spec.js#refuses
      a primary starting on the tenant's tomorrow, though it is already UTC's
      and Sydney's today
    origin: 'services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.md#D-12'
  - id: BR-CAP-CRM-ENT-010-02
    proved_by: [ATS-CRM-ENT-009-21, ATS-CRM-ENT-010-01, ATS-CRM-ENT-010-10, TST-CRM-ENT-790]
    statement: >-
      Affiliations are upcoming before their start, past after their inclusive
      end and current otherwise, including their last day.
    kind: calculation
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.business-dates.integration.spec.js#reads
      a tie ending on the tenant's today as current
    origin: 'services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.md#D-10'
  - id: BR-CAP-CRM-ENT-010-03
    proved_by: [ATS-CRM-ENT-010-02, TST-CRM-ENT-776]
    statement: >-
      Console affiliation filters, row labels and counts use the service’s
      effective-date classification, and new affiliations start with the
      workspace’s current date.
    kind: policy
    enforced_at:
      - console
    must_prove: true
    provenance: test
    source: >-
      react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0186.spec.ts#CRM-ACC-0186
    origin: 'services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.md#D-10'
  - id: BR-CAP-CRM-ENT-010-04
    proved_by: [ATS-CRM-ENT-010-03]
    statement: >-
      Date reconciliation processes starts after the last settled day through
      today and ends from the last settled day up to yesterday, begins from
      yesterday for an unprocessed workspace and includes missed days in the
      next window.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/unit/services/affiliations/affiliation-date-window.spec.js#spans
      every day of a skipped weekend, so the next night makes them up
    origin: >-
      services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.plan.md#A6
  - id: BR-CAP-CRM-ENT-010-05
    proved_by: [ATS-CRM-ENT-006-15, ATS-CRM-ENT-010-04, ATS-CRM-ENT-010-09, ATS-CRM-ENT-010-11, ATS-CRM-ENT-010-12, ATS-CRM-ENT-011-14, ATS-CRM-ENT-012-21, TST-CRM-ENT-776, TST-CRM-ENT-790]
    statement: >-
      Reconciliation assigns roles and pending default links for starts in its
      window, recalculates organizations affected by affiliation or role
      boundaries and otherwise does not save records merely because a day
      passes.
    kind: lifecycle
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.integration.spec.js#takes
      the organization out of compliance the next morning, with no save of the
      contact
    origin: >-
      services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.plan.md#A7
  - id: BR-CAP-CRM-ENT-010-06
    proved_by: [ATS-CRM-ENT-010-05, ATS-CRM-ENT-010-08, TST-CRM-ENT-790]
    statement: >-
      Repeating a settled day or processed window creates no further writes, and
      the reconciliation watermark advances only after all affected
      organizations settle successfully.
    kind: invariant
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.integration.spec.js#finds
      the day settled and changes nothing, the organization’s version included
    origin: >-
      services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.plan.md#A6
  - id: BR-CAP-CRM-ENT-010-07
    proved_by: [ATS-CRM-ENT-010-06, ATS-CRM-ENT-010-13, TST-CRM-ENT-786, TST-CRM-ENT-790]
    statement: >-
      On-demand reconciliation requires the contacts bulk permission; dry runs
      report changes without writing and may use a future day, while actual runs
      reject a supplied day, invalid calendar dates and unknown properties.
    kind: permission
    enforced_at:
      - api
      - service
    must_prove: true
    provenance: test
    source: >-
      services-crm:modules/entities/tests/e2e/affiliation-date-reconciliation.e2e.spec.js#refuses
      a real run that names a day, 400 naming it
    origin: >-
      services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.plan.md#A6
  - id: BR-CAP-CRM-ENT-010-08
    proved_by: [ATS-CRM-ENT-010-07]
    statement: >-
      Scheduled reconciliation processes workspaces after their local midnight
      only when enabled by deployment; the shipped default keeps it disabled.
    kind: lifecycle
    enforced_at:
      - service
    must_prove: true
    provenance: documented
    source: >-
      services-crm:docs/affiliations/README.md#5.6b The overnight date
      reconciliation (D-46; CRM-M2 A6 and A7)
    origin: >-
      services-crm:docs/milestones/CRM-M2-affiliations-roles-compliance.plan.md#A7
scenarios:
  - id: SC-CAP-CRM-ENT-010-01
    given: Ruth's Chapter affiliation starts next month.
    when: Her 360 and the chapter's Contacts tab are opened today.
    then: 'It displays under Upcoming, not Current, on both.'
    provenance: test
    source: >-
      react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0210.spec.ts#CRM-ACC-0210
  - id: SC-CAP-CRM-ENT-010-02
    given: >-
      A Billing Contact role ends yesterday on a current affiliation at an
      organization whose type needs one.
    when: The reconciliation runs this morning.
    then: The organization is out of compliance with no save of the contact.
    provenance: test
    source: >-
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.integration.spec.js#takes
      the organization out of compliance the next morning, with no save of the
      contact
edge_cases:
  - id: SC-CAP-CRM-ENT-010-03
    given: >-
      A workspace in New York one minute before its midnight, UTC already the
      next day.
    when: A affiliation starting on the New York tomorrow is read.
    then: It is upcoming.
    provenance: test
    source: >-
      services-crm:modules/entities/tests/unit/services/affiliations/affiliation-date-window.spec.js#is
      still the previous day in New York one minute before its midnight, when
      UTC has reached the next
constraints: []
depends_on:
  - CAP-CRM-ENT-009
related:
  - CAP-CRM-ENT-006
  - CAP-CRM-ENT-009
  - CAP-CRM-ENT-011
  - CAP-CRM-ENT-012
journeys:
  - JNY-CRM-ENT-002
  - JNY-CRM-ENT-005
open_questions: []
feature: FEAT-CRM-ENT-04
concepts:
  - CON-CRM-ENT-003
  - CON-CRM-ENT-033
evidence:
  - >-
    `services-crm` at `7ea48a414c42eade10d60753b8bcc323b1dd726f`: examined
    `services/affiliations/affiliation-date-reconciliation.ts`, `affiliation-date-window.ts`,
    `aws/api/contacts.routes.ts` (`reconcileAffiliationDates`),
    `affiliation-date-reconciliation.schemas.ts`, `db/crm-settings.table.ts` (the settled-day
    watermark), `docs/affiliations/README.md` §3.2a, §5.6b; tests
    `unit/services/affiliations/affiliation-date-window.spec.js`,
    `integration/services/affiliations/affiliation-date-reconciliation.integration.spec.js`,
    `affiliation-date-reconciliation.follow-ups.integration.spec.js`,
    `integration/services/persistence/contact-affiliations.business-dates.integration.spec.js`,
    `e2e/affiliation-date-reconciliation.e2e.spec.js`.
  - >-
    `react-console` at `da63075876b2c5f4a6af07d457bb24ea8bf4c0c7`: examined
    `components/affiliations/organization-affiliations-section.tsx`; journeys CRM-ACC-0186, 0210,
    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-005, 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]
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/persistence/contact-affiliations.business-dates.integration.spec.js#refuses
      a primary starting on the tenant's tomorrow, though it is already UTC's
      and Sydney's today;
      services-crm:modules/entities/tests/integration/services/persistence/contact-affiliations.business-dates.integration.spec.js#reads
      a tie ending on the tenant's today as current;
      react-console:acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0186.spec.ts#CRM-ACC-0186;
      services-crm:modules/entities/tests/unit/services/affiliations/affiliation-date-window.spec.js#spans
      every day of a skipped weekend, so the next night makes them up;
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.integration.spec.js#takes
      the organization out of compliance the next morning, with no save of the
      contact;
      services-crm:modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.integration.spec.js#finds
      the day settled and changes nothing, the organization’s version included;
      services-crm:modules/entities/tests/e2e/affiliation-date-reconciliation.e2e.spec.js#refuses
      a real run that names a day, 400 naming it;
      services-crm:docs/affiliations/README.md#5.6b The overnight date
      reconciliation (D-46; CRM-M2 A6 and A7)
    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/affiliations/README.md
      - modules/entities/tests/e2e/affiliation-date-reconciliation.e2e.spec.js
      - >-
        modules/entities/tests/integration/services/affiliations/affiliation-date-reconciliation.integration.spec.js
      - >-
        modules/entities/tests/integration/services/persistence/contact-affiliations.business-dates.integration.spec.js
      - >-
        modules/entities/tests/unit/services/affiliations/affiliation-date-window.spec.js
  - repo: react-console
    role: console
    paths:
      - 'app/(console)/console/[tenantId]/crm/contacts/'
      - acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0186.spec.ts
      - acceptance/crm/playwright/journeys/affiliations/CRM-ACC-0210.spec.ts
exercised_by: [FAS-CRM-ENT-04-01, FAS-CRM-ENT-04-02, FAS-CRM-ENT-04-03, FAS-CRM-ENT-04-04, FAS-CRM-ENT-04-05]
suites: [TST-CRM-ENT-776, TST-CRM-ENT-786, TST-CRM-ENT-790]
---

# Keep dated relationships current

## Purpose

A relationship can begin or end even when no staff member edits it that day. This capability gives dated affiliations a consistent workspace calendar and processes the side effects of crossed boundaries, so current labels, automatic roles, shared-detail defaults, and compliance assessments do not depend on the browser's or server's local date.

## How It Works

Rhythm classifies affiliations as upcoming, current, or past using the workspace's business day. The end day remains current. Console counts and filters use the same classification, and new ties start with that workspace day unless staff choose another supported date.

Reconciliation processes the interval since the last settled day rather than only today's boundaries. It applies role and pending default-link work for starts and recalculates organizations affected by affiliation or role boundaries. The watermark advances after all affected organizations settle; repeating an already settled interval creates no further writes. Merely passing a day does not resave every record.

An authorized on-demand run can reconcile real work or report a dry run. Dry runs may inspect a future day without writing, while actual runs use the current workspace day. Scheduled processing runs after local midnight only when deployment enables it; the pinned default leaves it disabled. Thus classification can be correct on a read while deferred boundary side effects still need reconciliation. Automatic-role settlement also has a separate due-role pass; disabling the date-window sweep does not itself establish that this independent pass is disabled.

## Example

Lena's affiliation starts tomorrow with a pending work-phone link and automatic roles. Today staff see an upcoming tie without those start-day effects. After an enabled reconciliation processes the start day, eligible defaults and roles are applied and relevant organizations are reevaluated. Repeating that settled run does not add another role or revision. A missed processing day is included in the next interval rather than discarded.

Lena's association uses a business day that can differ from UTC. The coordinator checks the service's effective-day classification before interpreting Tomorrow in her own browser. For a tie ending October 23, the Current view includes that final day and the Past view applies on October 24. An administrator can inspect a dry run for October 24 without writing role or compliance changes. A real run cannot use that supplied future date. If processing misses the weekend, the next unsettled window includes the crossed boundaries; a repeated settled run changes neither the organization version nor its history. These inspections distinguish calendar classification from completed side effects.

## Why It Works This Way

Our reading is that a workspace calendar is necessary when staff and infrastructure occupy different time zones; an effective business day should not change with the machine asking the question. Inclusive end dates align the final day of a relationship with staff expectations and the formal period model.

A watermark protects missed work and repeat safety together. Advancing it before every affected organization settles would make a later retry skip unfinished effects. Limiting writes to actual boundary work avoids filling history with daily no-ops. Dry-run freedom and real-run date restrictions separate planning from mutation. The disabled scheduler default is a deployment boundary, not evidence that the product intends boundary side effects never to run.

## Relationships

Maintain a person’s organizational relationships owns periods and primary constraints. Maintain responsibilities on organizational relationships and Share organization contact points with affiliated people supply start-day actions. Review organization role compliance and prepare notices owns reevaluation. CRM Settings holds the settled-day watermark, while deployment enables the scheduled worker; no inference operation participates in this processing.
