---
id: CAP-MBR-STR-015
title: Determine a term’s start and end dates
feature: FEAT-MBR-STR-05
product_area: membership
module: structure
revision: 1
status: draft
product_intent: proposed
implementation_evidence: observed
supersedes:
  - react-console acceptance inventory CAP-MBR-CFG-005
source_sha:
  services-membership: 5994c270e8a37a30f848e99ba5b5ee6b87c08893
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
intent_sources:
  - >-
    services-membership:README.md#2026-10-18 — Final acceptance, Decisions (Andrew: a calendar period ending
    in February ends on the month's real last day)
  - >-
    services-membership:README.md#The term engine (D-45, decided 2026-10-17: the engine imports nothing of
    Membership and is extracted later)
purpose: Rhythm derives a membership period from the offering’s date rule and the term being continued.
outcomes:
  - Date calculations produce repeatable periods from explicit inputs.
  - Invalid date-rule combinations are rejected with actionable problems.
concepts:
  - CON-MBR-STR-005
  - CON-MBR-STR-004
actors:
  - ACT-MBR-001
  - ACT-MBR-004
surfaces:
  - api
  - job
preconditions: []
postconditions: []
permissions:
  - key: membership.configuration.manage
    where_checked: api
    scope: tenant
  - key: membership.application.access
    where_checked: api
    scope: tenant
rules:
  - id: BR-CAP-MBR-STR-015-01
    statement: >-
      Term rules use anniversary, calendar or never-ending bases, require valid length and units where
      applicable and report incompatible or missing settings together.
    kind: validation
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-membership:modules/terms/tests/unit/term-rules.spec.ts#term rules: what 2.0 decides › reports
      every problem of a rule at once
    origin: none
  - id: BR-CAP-MBR-STR-015-02
    statement: >-
      Partial package changes validate the combined stored and submitted term rule rather than only submitted
      settings.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: observed
    source: >-
      services-membership:modules/configuration/services/configuration/configuration-records.ts:packageResource
    origin: none
  - id: BR-CAP-MBR-STR-015-03
    statement: >-
      A continuing term begins the day after the existing term’s end regardless of purchase date unless
      continuation is disabled.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-membership:modules/terms/tests/unit/term-rules.spec.ts#term rules: the rules of 1.0 beyond its
      spec cases › extends an early renewal from the current end, whatever the basis
    origin: none
  - id: BR-CAP-MBR-STR-015-04
    statement: >-
      A terminated term continues through its end plus the reinstatement allowance, treating absence as zero
      days; purchases after that boundary restart.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: observed
    source: 'services-membership:modules/terms/services/term-rules.ts:startingPoint'
    origin: none
  - id: BR-CAP-MBR-STR-015-05
    statement: >-
      Anniversary rules apply their month-end rounding and new-term sell-after threshold to year, quarter and
      month terms, while day and week terms retain the full count.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-membership:modules/terms/tests/unit/term-rules.spec.ts#term rules: 1.0
      calculate-expiration-date.spec.js › 1.0 lines 284-292: anniversary in years, should calculate properly
      for a single year › ends a purchase of 2019-04-03 at the end of the anniversary month, 2020-04-30
    origin: none
  - id: BR-CAP-MBR-STR-015-06
    statement: >-
      Fixed windows match an adjusted anniversary start; an immediately following date moves back only when
      the window end is in that start’s year, excluding the January-after-December boundary.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: observed
    source: 'services-membership:modules/terms/services/term-rules.ts:fixedWindowProbe'
    origin: none
  - id: BR-CAP-MBR-STR-015-07
    statement: >-
      Calendar rules end before their start month, use the sell-for-next-year threshold and backdate a fresh
      term to the period start while retaining a continuation’s start.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-membership:modules/terms/tests/unit/term-rules.spec.ts#term rules: 1.0
      calculate-expiration-date.spec.js › 1.0 lines 70-77: calendar, a standard year, should calculate
      properly for April › back-dates a purchase of 2019-04-03 to 2019-01-01 and ends it 2019-12-31
    origin: none
  - id: BR-CAP-MBR-STR-015-08
    statement: 'A calendar period finishing in February uses the actual last day, including leap day.'
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-membership:modules/terms/tests/unit/term-rules.spec.ts#term rules: what 2.0 decides › ends a
      calendar period ending in February on its last day in a leap year: 2024-02-29 (decided by Andrew
      2026-10-18: the real last day of the month; 1.0 added a year with Moment and ended it 2024-02-28)
    origin: >-
      services-membership:README.md#2026-10-18 — Final acceptance, Decisions (Andrew: a calendar period ending
      in February ends on the month's real last day)
  - id: BR-CAP-MBR-STR-015-09
    statement: A never-ending rule returns its starting point with no end date.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-membership:modules/terms/tests/unit/term-rules.spec.ts#term rules: 1.0
      calculate-expiration-date.spec.js › 1.0 lines 248-255: no expiration, should calculate properly › starts
      on 2019-04-03 and never ends
    origin: none
  - id: BR-CAP-MBR-STR-015-10
    statement: Date computation uses supplied inputs without reading a clock or mutating those inputs.
    kind: invariant
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-membership:modules/terms/tests/unit/term-rules.spec.ts#term rules: what 2.0 decides › gives the
      same result twice for the same frozen inputs (1.0 turned the fixed dates into Moments in place, so a
      second call read other inputs)
    origin: none
  - id: BR-CAP-MBR-STR-015-11
    statement: The term engine imports no other Membership module so it can be extracted when another domain adopts it.
    kind: invariant
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-membership:modules/terms/tests/unit/isolation.spec.ts#term engine isolation › imports nothing
      of the repository outside modules/terms
    origin: >-
      services-membership:README.md#The term engine (D-45, decided 2026-10-17: the engine imports nothing of
      Membership and is extracted later)
  - id: BR-CAP-MBR-STR-015-12
    statement: Overlapping fixed windows are accepted and resolved by the first matching window in stored order.
    kind: policy
    enforced_at:
      - service
    must_prove: false
    provenance: observed
    source: 'services-membership:modules/terms/services/term-rules.ts:fixedTermEnd'
    origin: none
  - id: BR-CAP-MBR-STR-015-13
    statement: >-
      Equal start and end month-days create a year-spanning fixed window, with the result after the window end
      and additional years for multi-year terms.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: observed
    source: 'services-membership:modules/terms/services/term-rules.ts:fixedTermEnd'
    origin: none
scenarios:
  - id: SC-CAP-MBR-STR-015-01
    given: A current annual term ends on the last day of June.
    when: The member purchases its continuation during May.
    then: The new period starts on the first day of July.
    provenance: test
    source: >-
      services-membership:modules/terms/tests/unit/term-rules.spec.ts#term rules: the rules of 1.0 beyond its
      spec cases › extends an early renewal from the current end, whatever the basis
edge_cases:
  - id: SC-CAP-MBR-STR-015-02
    given: Two fixed windows end October-to-March purchases in June and April-to-September purchases in December.
    when: A first-time member purchases on the first day of April.
    then: The boundary adjustment selects the earlier window and a June end.
    provenance: documented
    source: 'react-console:acceptance/membership/journeys/configuration/MBR-ACC-0100.json'
constraints: []
depends_on:
  - CAP-MBR-STR-012
related:
  - CAP-MBR-STR-013
  - CAP-MBR-STR-016
journeys:
  - JNY-MBR-STR-005
open_questions:
  - OQ-MBR-STR-005
  - OQ-MBR-STR-006
evidence:
  - >-
    services-membership: Examination of the term rule computation and its checks, the package term columns and
    their database checks.
  - >-
    services-membership: Examination of the term rule unit suite (Rhythm 1.0’s 74 cases and 2.0’s decisions),
    its property suite and the isolation guard.
  - 'services-membership: Examination of the bootstrap record and the 2026-10-18 acceptance decisions.'
  - 'services-membership:modules/configuration/services/configuration/configuration-records.ts'
  - 'services-membership:modules/terms/services/term-rules.ts'
  - 'services-membership:modules/terms/tests/unit/isolation.spec.ts'
  - 'services-membership:modules/terms/tests/unit/term-rules.spec.ts'
controls: [TC-AZ-001, TC-AZ-002, TC-AZ-003, TC-AZ-004, TC-AZ-005, TC-AZ-006, TC-CHG-001, TC-CHG-002, TC-CHG-003, TC-CHG-004, TC-IAM-001, TC-IAM-002, TC-IAM-004, TC-IAM-005, TC-LOG-004, TC-PI-001, TC-SEC-001, TC-SEC-004, TC-TEN-001, TC-TEN-002, TC-TEN-003]
---

# Determine a term’s start and end dates

## Purpose

Rhythm derives a membership period from the offering’s date rule and the term being continued.

## How It Works

The caller supplies a package rule, a business date and any current term being continued. The engine first determines whether the purchase continues, reinstates or starts a new period. It rejects a rule that cannot be computed. Anniversary rules then apply fixed windows or length and rounding settings. Calendar rules align the period with the configured start month. Never-ending rules return no end date. The computation uses the supplied calendar values rather than an ambient clock. Repeating identical inputs therefore produces identical dates. Package edits are checked against the resulting complete rule. Fixed-window ordering and boundary handling can produce surprising first terms. The product should resolve those boundary choices and the meaning of an empty reinstatement allowance before migrating packages that relied on different meanings.

## Relationships

- Packages and independently dated grants provide term rules.
- Status calculations interpret the resulting periods alongside term states.
