Skip to content

Expand recurring series in the zone their invites are published in - #455

Merged
jhodapp merged 4 commits into
mainfrom
fix/series-dst-local-expansion
Oct 21, 2026
Merged

jhodapp merged 4 commits into
mainfrom
fix/series-dst-local-expansion

Conversation

@jhodapp

@jhodapp jhodapp commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Description

Recurring series were expanded in naive UTC, while the .ics anchors the series in the coach's zone (DTSTART;TZID + RRULE). Calendars keep the local wall time across DST, so after a transition every occurrence was stored an hour off the calendar. Single-occurrence reschedules and cancels then named a RECURRENCE-ID no client held, and Google dropped them (production repro: Nov 5 occurrence of series 67d9d4a5).

This expands series on local wall time in the zone the series was published in, exactly as RFC 5545 clients do, and stores that zone on the series so later invites and expansions cannot split apart.

GitHub Issue: Fixes #456

Changes

  • expand_recurrence(start_at, rule, zone) steps on local wall time and resolves each slot to UTC per RFC 5545 §3.3.5 (a skipped time uses the offset before the gap, a repeated time its first pass). until is compared against the resolved instant.
  • Weekdays and month days are validated in local time, so an evening series (Thu 7 PM Chicago, Friday in UTC) no longer gets a 422.
  • New 422s: a monthly series starting after day 28 in the coach's zone (clients disagree on short months), and a start in the second pass of a repeated hour.
  • SeriesRule.timezone: the zone actually emitted (UTC when no VTIMEZONE exists), set at create and reschedule. Every invite under the series UID (master, occurrence move and cancel, snapshot resend, series cancel) uses it, even if the coach later changes zone.
  • Migration backfills rule.timezone on existing series from the coach's current zone (unknown names become UTC). down removes the key.
  • RRULE now carries WKST=MO.
  • Manual test plan: new "Series across a DST change" section.

Testing Strategy

  • Unit tests for every frequency across fall-back and spring-forward in Chicago, London, Sydney, Kolkata and UTC, plus evening and morning weekday series, biweekly with Sunday, the until boundary, gap, overlap and the new 422s.
  • Oracle test: the rrule crate (already in the tree via icalendar, added as a dev-dependency) expands the exact DTSTART + RRULE we emit across 20 cases, and must equal our slots. Mutation-checked: it fails if expansion reverts to UTC or the overlap rule changes.
  • Stored-zone tests: a coach who moved from Chicago to New York still gets Chicago RECURRENCE-ID/EXDATE/TZID on occurrence move, occurrence cancel and the snapshot.
  • Migration run up, down and up against local Postgres; down restores every rule exactly.
  • Live: the new HD/SD cases in docs/test-plans/ics-calendar-invites-manual-test-plan.md, run with a Chicago coach against Google Calendar.

Concerns

  • Existing drifted rows are not changed by this PR. A one-time repair (run separately after deploy, with a dry run first) realigns them and resends where a calendar update was dropped. It must run before about Oct 31, when reminders for post-DST sessions start going out.
  • Create and reschedule now read the coach's user row once more to resolve the zone.

Series slots were stepped in naive UTC while the .ics anchors the RRULE in the
coach's zone, so after a DST change every occurrence was stored an hour off the
calendar and per-occurrence overrides and cancels named instances no client held.

- Expand on local wall time in the series' zone and resolve each slot to UTC per
  RFC 5545 (skipped time uses the pre-gap offset, repeated time its first pass).
- Validate weekdays and month days locally, so evening series no longer 422.
- Reject monthly series starting after day 28 and starts in the second pass of
  a repeated hour.
- Persist the published zone on SeriesRule (backfilled from the coach) and use
  it for every invite under the series UID.
- Emit WKST=MO.
- Check slots against the rrule crate's expansion of the emitted invite.
@jhodapp jhodapp self-assigned this Oct 20, 2026
@jhodapp jhodapp added enhancement Improves existing functionality or feature bug fix Contains a fix to a known bug labels Oct 20, 2026
@jhodapp jhodapp added this to the 1.0.0-beta4 milestone Oct 20, 2026
@jhodapp jhodapp changed the title fix: expand recurring series in the zone their invites are published in Expand recurring series in the zone their invites are published in Oct 20, 2026
@jhodapp
jhodapp marked this pull request as ready for review October 20, 2026 21:09
@greptile-apps

greptile-apps Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The changes affect recurring coaching schedules and their calendar updates.

The PR still needs the stored monthly-series resend fix completed before merging.

Fix All in Claude CodeFindings

  1. P1 Existing series cannot resend ▶

Summary

Recurring sessions now keep their local start time across daylight-saving changes.

  • Recurring sessions keep their local time across daylight-saving changes.
  • Series invites keep using the timezone they were first published in.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Create or reschedule] --> B[Resolve coach publication zone]
    B --> C[Validate and expand in local time]
    C --> D[Store rule, zone, and UTC sessions]
    D --> E[Build invites using stored zone]
    F[Resend stored series] --> G[Expand published rule]
    G --> H[Match stored sessions to slots]
    H --> E
Loading

Reviews (2) · Last reviewed commit: "fix: expand stored series without the ch..." · Reviewed by Greptile

Comment thread domain/src/coaching_session/recurrence.rs
Comment thread domain/src/coaching_session/recurrence.rs
Comment thread domain/src/coaching_session/recurrence_dst_tests.rs Outdated
Snapshots expanded stored rules through the publish-time validation, so a
monthly series on day 29-31, or one accepted under the old UTC weekday check,
could no longer be resent. Stored rules now expand with structural checks only,
and months without the start day are skipped as RFC 5545 clients skip them.

Also orders the new test files' imports external-first.
@jhodapp
jhodapp merged commit ec595ba into main Oct 21, 2026
8 checks passed
@jhodapp
jhodapp deleted the fix/series-dst-local-expansion branch October 21, 2026 14:27
@github-project-automation github-project-automation Bot moved this from Review to ✅ Done in Refactor Coaching Platform Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug fix Contains a fix to a known bug enhancement Improves existing functionality or feature

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

Recurring series drift an hour from their calendar invites after a DST change

1 participant