---
id: CAP-MBR-MEM-020
title: Tell other applications when membership changes
feature: FEAT-MBR-MEM-07
product_area: membership
module: memberships
revision: 1
status: draft
product_intent: proposed
implementation_evidence: partially-observed
supersedes:
  - 'inventory:CAP-MBR-DAT-011'
  - 'inventory:CAP-MBR-DAT-012'
  - 'inventory:CAP-MBR-DAT-014'
source_sha:
  services-membership: 5994c270e8a37a30f848e99ba5b5ee6b87c08893
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
intent_sources: []
purpose: >-
  Applications that depend on membership, Committees first, hear of every term committed, restated, ended or
  cancelled, every chapter tie started or ended, every suspension and every structural move, reliably and once.
outcomes:
  - Committees can end a person's appointments when their membership ends.
  - Any application can follow membership without reading Membership's records.
concepts:
  - CON-MBR-MEM-006
  - CON-MBR-MEM-002
actors:
  - ACT-MBR-012
  - ACT-MBR-011
surfaces:
  - job
permissions: []
rules:
  - id: BR-CAP-MBR-MEM-020-01
    proved_by: [ATS-MBR-MEM-004-07, ATS-MBR-MEM-006-19, ATS-MBR-MEM-014-16, ATS-MBR-MEM-020-01, ATS-MBR-MEM-020-09, ATS-MBR-MEM-020-11, ATS-MBR-MEM-020-12, ATS-MBR-MEM-020-13, ATS-MBR-MEM-020-17, TST-MBR-MEM-014]
    statement: >-
      Membership states nine business events (a term committed, restated, terminated or cancelled; an affiliation
      started or ended; a membership suspended or restored; a unit moved in the tree), each in the transaction of the
      write it describes, so an operation that rolls back states nothing and none is stated outside a transaction.
    kind: invariant
    enforced_at: [service, database]
    must_prove: true
    provenance: test
    source: >-
      services-membership:modules/events/tests/integration/business-events.integration.spec.ts#leaves no event when the
      operation that stated it rolls back
    origin: none
  - id: BR-CAP-MBR-MEM-020-02
    proved_by: [ATS-MBR-MEM-020-02, TST-MBR-MEM-014, TST-MBR-MEM-035]
    statement: >-
      Every membership event names the membership, its program, whether a person or an organization holds it (the person
      only for a person's), its origin and parent, and the person, operation and source of the change.
    kind: policy
    enforced_at: [service]
    must_prove: true
    provenance: test
    source: >-
      services-membership:modules/memberships/tests/unit/membership-business-events.spec.ts#names an organization’s
      membership by its organization and no contact_id, whoever its primary contact is
    origin: none
  - id: BR-CAP-MBR-MEM-020-03
    proved_by: [ATS-MBR-MEM-003-13, ATS-MBR-MEM-005-12, ATS-MBR-MEM-013-17, ATS-MBR-MEM-020-03, ATS-MBR-MEM-020-08, ATS-MBR-MEM-020-10, ATS-MBR-MEM-020-16, TST-MBR-MEM-014, TST-MBR-MEM-035]
    statement: >-
      A termination gives the last day of rights, the first day without and the reason; a lapse found by the nightly
      sweep is stated once as a termination ending on the term's own last day.
    kind: calculation
    enforced_at: [service]
    must_prove: true
    provenance: test
    source: >-
      services-membership:modules/events/tests/integration/business-events.integration.spec.ts#states
      membership.term.terminated once when the midnight sweep finds a term lapsed, with the term’s last day
    origin: none
  - id: BR-CAP-MBR-MEM-020-04
    proved_by: [ATS-MBR-MEM-009-20, ATS-MBR-MEM-010-13, ATS-MBR-MEM-020-04, ATS-MBR-MEM-020-14, ATS-MBR-MEM-020-15]
    statement: A cancellation says whether it removed the membership along with the term.
    kind: policy
    enforced_at: [service]
    must_prove: true
    provenance: test
    source: >-
      services-membership:modules/memberships/tests/unit/membership-business-events.spec.ts#says whether a cancellation
      removed the membership
    origin: none
  - id: BR-CAP-MBR-MEM-020-05
    proved_by: [ATS-MBR-MEM-020-05, TST-MBR-MEM-016]
    statement: >-
      Each event's content is strict and versioned: an undeclared field, a date that is not a local date, an overlong id
      or a termination without its last day is refused.
    kind: validation
    enforced_at: [service, database]
    must_prove: true
    provenance: test
    source: >-
      services-membership:modules/events/tests/unit/business-event-schemas.spec.ts#refuses a termination that names no
      last day
    origin: none
  - id: BR-CAP-MBR-MEM-020-06
    proved_by: [ATS-MBR-MEM-020-06, TST-MBR-MEM-015, TST-MBR-MEM-239]
    statement: >-
      Events are delivered at least once and in no guaranteed order, each with its own stable identifier; Committees is
      handed terminations alone, and other events are settled without calling it.
    kind: policy
    enforced_at: [service]
    must_prove: true
    provenance: test
    source: >-
      services-membership:modules/events/tests/integration/change-events-dispatch.integration.spec.ts#gives Committees
      the business event’s own id, so a redelivery names the same event
    origin: none
  - id: BR-CAP-MBR-MEM-020-07
    proved_by: [ATS-MBR-MEM-020-07, TST-MBR-MEM-015]
    statement: >-
      A delivery a receiver fails is retried, and after five failed attempts it is set aside for operators with an alarm.
    kind: lifecycle
    enforced_at: [service]
    must_prove: true
    provenance: test
    source: >-
      services-membership:modules/events/tests/integration/change-events-dispatch.integration.spec.ts#reports a failure
      on the queue’s last receive too, which moves the message to the dead-letter queue
    origin: none
  - id: BR-CAP-MBR-MEM-020-08
    proved_by: []
    statement: >-
      When an environment names no Committees receiver, events are kept as Membership's record but delivered to no one.
    kind: policy
    enforced_at: [service]
    must_prove: false
    provenance: test
    source: >-
      services-membership:modules/events/tests/unit/change-event-registry.spec.ts#registers no handler without the ARN,
      so the stream’s events are deleted as they are claimed
    origin: none
  - id: BR-CAP-MBR-MEM-020-09
    proved_by: []
    statement: >-
      Bulk imports, the trimming of affiliations to a corrected term, and the void of a never-committed term state no
      event.
    kind: policy
    enforced_at: [service]
    must_prove: false
    provenance: documented
    source: 'services-membership:docs/migrations/events/README.md#What could not be done as specified, and why'
    origin: none
  - id: BR-CAP-MBR-MEM-020-10
    proved_by: []
    statement: >-
      The product should move a merged-away contact's or organization's memberships to the surviving record when CRM
      merges two records.
    kind: policy
    enforced_at: [service]
    must_prove: true
    provenance: unknown
    source: FND-MBR-MEM-064
    origin: none
  - id: BR-CAP-MBR-MEM-020-11
    proved_by: []
    statement: >-
      Membership reads CRM's people, Accounting's products and prices and Platform's workspaces, users and number seeds
      directly from their owners' tables.
    kind: policy
    enforced_at: [database]
    must_prove: false
    provenance: observed
    source: 'services-membership:modules/memberships/services/persistence/crm-people.ts'
    origin: none
scenarios:
  - id: SC-CAP-MBR-MEM-020-01
    given: Dana's term is terminated by staff with a last day of 30 September 2026.
    when: The dispatcher and consumer run.
    then: Committees receives exactly one termination naming Dana and 30 September, attributed to the termination.
    provenance: test
    source: >-
      services-membership:tests/e2e/business-events.e2e.spec.ts#states exactly one membership.term.terminated, with Dana’s
      contact_id and the last day of rights
edge_cases: []
constraints:
  - An event's content is at most 8 KB.
depends_on: []
related:
  - CAP-MBR-MEM-003
  - CAP-MBR-MEM-007
  - CAP-MBR-MEM-008
journeys: []
open_questions:
  - OQ-MBR-MEM-005
  - OQ-MBR-MEM-006
evidence:
  - repository: services-membership
    source_sha: 5994c270e8a37a30f848e99ba5b5ee6b87c08893
    paths:
      - modules/events/contracts/business-events.ts
      - modules/events/services/publishing/publish-business-event.ts
      - modules/events/services/handlers
      - modules/events/db/published-events.table.ts
      - modules/events/db/published-event-stream.ts
      - modules/memberships/services/events/membership-business-events.ts
      - aws/membership.change-events.ts
      - modules/events/tests
      - modules/memberships/tests/unit/membership-business-events.spec.ts
      - tests/e2e/business-events.e2e.spec.ts
      - docs/migrations/events/README.md
  - repository: react-console
    source_sha: baf6dae382d6f41dce976c7f848667ccc37e076f
    paths:
      - acceptance/membership/inventory/business-rules.json
      - acceptance/membership/reports/defects/data-operations.md
controls: [TC-AVL-002, TC-AVL-003, TC-AVL-004, TC-AZ-001, TC-AZ-003, TC-AZ-004, TC-CHG-001, TC-CHG-002, TC-CHG-003, TC-CHG-004, TC-CRY-001, TC-CRY-002, TC-CRY-003, TC-DAT-001, TC-DAT-002, TC-DAT-003, TC-DAT-007, TC-IAM-006, TC-LOG-002, TC-LOG-003, TC-LOG-005, TC-MON-001, TC-MON-002, TC-MON-004, TC-PI-001, TC-PI-002, TC-PI-004, TC-PRV-001, TC-PRV-002, TC-SEC-001, TC-SEC-004, TC-TEN-001, TC-TEN-002, TC-TEN-003]
exercised_by: [FAS-MBR-MEM-07-01, FAS-MBR-MEM-07-03]
suites: [TST-MBR-MEM-014, TST-MBR-MEM-015, TST-MBR-MEM-016, TST-MBR-MEM-035, TST-MBR-MEM-239]
---

# Tell other applications when membership changes

## Purpose

Other applications act on membership. Committees ends a person's appointments when their membership ends, and others will follow. They must hear of each change reliably, without reading Membership's records themselves.

## How It Works

Whenever Membership commits a change that matters to others, it states a business event in the same step. The changes are a term committed, restated, ended or cancelled, a chapter tie started or ended, a membership suspended or restored, and a unit moved in the tree. Each event names the membership, its program, its holder, its origin, and who made the change and how. A termination says the last day of rights and the first day without. A lapse found overnight is reported as a termination ending on the term's own last day. A minute-by-minute dispatcher delivers events at least once, unordered, each with a stable identifier. Committees is handed terminations only. A failed delivery is retried, and after five failures it is set aside with an alarm. Events are kept as Membership's own record. Imports and some automatic trims state no event. This behaviour was built after Andrew's acceptance of Membership and uses a delivery mechanism that differs from Rhythm's integration catalogue, so its intent is proposed. The product should also move memberships when CRM merges two people; today a merged-away person's memberships stay on the deleted record.

## Relationships

- Related to `CAP-MBR-MEM-003`, `CAP-MBR-MEM-007` and `CAP-MBR-MEM-008` because their writes state the events.
- Depends on Committees (`MOD-CMT-CMT`) as the first receiver, on Platform's integrations (`MOD-PFM-INT`) for the event catalogue, and on CRM data quality (`MOD-CRM-DQ`) for record merges.
