Skip to content

Add persistent RSVP cards and reconciliation for scheduled Party sessions - #70

Merged
diese-tech merged 2 commits into
mainfrom
claude/issue-67-schedule-rsvp-cards
Aug 23, 2026
Merged

Add persistent RSVP cards and reconciliation for scheduled Party sessions#70
diese-tech merged 2 commits into
mainfrom
claude/issue-67-schedule-rsvp-cards

Conversation

@diese-tech

Copy link
Copy Markdown
Owner

Closes #67.

Summary

Confirming a scheduled night now auto-publishes a persistent, button-first RSVP card (Going / Maybe / Can't Make It) to the Play channel instead of requiring players to know an RSVP command. First-time Going shows the same lightweight Primary/Secondary/Fill picker as joining an ad-hoc queue; a returning player with saved roles RSVPs in one click with a Change Roles escape hatch. Maybe is a genuinely separate response that never occupies a seat or waitlist slot. Every RSVP mutation — from the card or the /party rsvp/unrsvp fallback commands — refreshes the one canonical card via its durable delivery reference, never whichever message triggered the interaction.

A weekly night rolls the same Discord message forward to each next occurrence with a fresh roster instead of posting a new card every week; RSVP history for the occurrence that just converted stays durable on its own row for audit even though the public projection has moved on. A one-time night's card gets one final edit into a "session started" terminal state instead. Startup recovery and every periodic sweep reconcile every guild's scheduled-night cards (post-if-missing, repost-if-deleted, never duplicated when still live); a new /party session-refresh command provides the same reconciliation for one named event on demand, gated to its organizer or a server manager.

Player/organizer-facing UX changes

  • Confirming a scheduled night immediately posts an RSVP card — no separate share step.
  • Going/Maybe/Can't Make It are buttons on that card; /party rsvp/unrsvp remain working fallbacks.
  • A weekly series keeps one card across occurrences instead of accumulating a new one every week.
  • New /party session-refresh EVENT_ID admin/organizer recovery command.

Domain changes (utils/party_schedule.py)

  • ScheduledNight gains delivery_channel_id/delivery_message_id (the durable card location) and a maybe tuple, tracked entirely separately from rsvps/waitlist.
  • scheduled_rsvps gets a response column (going/maybe); only going responses ever occupy a seat or waitlist position. rsvp() now promotes an existing Maybe to Going; new rsvp_maybe() releases (and reindexes) any held Going seat first, since Maybe is never a stronger commitment than Going. cancel_rsvp() handles both.
  • mark_converted() carries the delivery ref forward to the newly created weekly successor row (tracked via a new predecessor_event_id column) in the same transaction, and clears it on the now-CONVERTED row — this is what lets the RSVP card follow the series instead of accumulating. New find_successor() and list_all_upcoming() (cross-guild, for the reconciliation sweep).

New modules

utils/schedule_rsvp.py (ScheduleRsvpService) and utils/schedule_views.py (ScheduleRsvpView) mirror the existing PartyLobbyService/RecruitingCardView pattern: injected Discord-UI-agnostic collaborators, embed-footer-based event_id routing, and the same ensure/refresh-card idioms already established for the queue card.

schedule_commands.py wires auto-publish into confirm, card refresh into rsvp/unrsvp, rollover-or-terminal-refresh into open-scheduled, and the new session-refresh command. ScheduleLifecycle gained on_startup and extended on_cleanup to sweep list_all_upcoming() through the same reconcile() path used by recovery — the new rsvp_service collaborator is optional so pre-existing reminder-only callers/tests are unaffected.

Tests

New tests/unit/test_schedule_rsvp_cards.py (18 tests) plus updates to test_party_schedule.py and test_party_schedule_commands_characterization.py, covering: auto-publish on confirm, first-time vs returning RSVP, Maybe (including releasing a held seat and promoting the waitlist), Can't Make It, card refresh via delivery ref regardless of trigger message, reconciliation (post-if-missing/repost-if-deleted/no-duplicate-if-live/skip-unconfirmed), the cross-guild periodic/startup sweep, the session-refresh command's permission gate and both outcomes, weekly rollover (same message, fresh roster, historical data preserved), one-time terminal-state conversion, and multi-session isolation.

Full suite: 689 passing.

Version

Bumped to 2.3.0-rc.5 per RELEASE_PROCESS.md.

Manual Discord test checklist

  1. /party schedule a one-time night, then /party confirm it — confirm an RSVP card is posted to the Play channel with Going/Maybe/Can't Make It buttons.
  2. As a first-time player, click Going — confirm the lightweight role wizard appears; submit it and confirm you're RSVP'd and the card updates.
  3. As a returning player with saved roles, click Going — confirm it RSVPs instantly with a Change Roles option, and the card updates.
  4. Click Maybe — confirm it's recorded without a wizard and doesn't count toward capacity.
  5. RSVP as Going until the roster is full, then have another player click Maybe after already being Going — confirm their seat is released and the next waitlisted player is promoted.
  6. Click Can't Make It — confirm the RSVP/Maybe response is removed and the card updates.
  7. Use /party rsvp//party unrsvp instead of the buttons — confirm the same public card still updates.
  8. Manually delete the RSVP card message, then run /party session-refresh EVENT_ID — confirm exactly one replacement card is posted. Run it again while the card is still live — confirm it's reconciled in place, not duplicated.
  9. Restart the bot with an unconfirmed session, a confirmed session with a live card, and a confirmed session with a manually deleted card — confirm only the two confirmed sessions get cards, and no duplicates appear for the live one.
  10. /party schedule a weekly night, confirm it, RSVP a few players, then /party open-scheduled it — confirm the same card message now shows the next week's occurrence with an empty roster, and that the original occurrence's RSVP data is still queryable for history.
  11. Convert a one-time night via /party open-scheduled — confirm its card is edited to a "session started" state with no buttons, linking to the lobby if resolvable.
  12. Run two scheduled nights in the same guild concurrently — confirm RSVPing to one never touches the other's card.

🤖 Generated with Claude Code


Generated by Claude Code

…ions

Closes #67.

Confirming a scheduled night now auto-publishes a persistent, button-
first RSVP card (Going/Maybe/Can't Make It) to the Play channel instead
of requiring players to know an RSVP command. First-time Going shows
the same lightweight Primary/Secondary/Fill picker as joining an ad-hoc
queue; a returning player with saved roles RSVPs in one click with a
Change Roles escape hatch. Maybe is a genuinely separate response that
never occupies a seat or waitlist slot. Every RSVP mutation - from the
card or the /party rsvp/unrsvp fallback commands - refreshes the one
canonical card via its durable delivery reference, never whichever
message triggered the interaction.

A weekly night rolls the *same* Discord message forward to each next
occurrence with a fresh roster instead of posting a new card every
week; RSVP history for the occurrence that just converted stays
durable on its own row for audit even though the public projection has
moved on. A one-time night's card gets one final edit into a "session
started" terminal state instead. Startup recovery and every periodic
sweep reconcile every guild's scheduled-night cards (post-if-missing,
repost-if-deleted, never duplicated when still live); the new
`/party session-refresh` command provides the same reconciliation for
one named event on demand, gated to its organizer or a server manager.

Domain changes (utils/party_schedule.py):
- ScheduledNight gains delivery_channel_id/delivery_message_id (the
  durable card location) and a `maybe` tuple, tracked entirely
  separately from rsvps/waitlist.
- scheduled_rsvps gets a `response` column (going/maybe); only `going`
  responses ever occupy a seat or waitlist position. rsvp() now
  promotes an existing Maybe to Going; new rsvp_maybe() releases (and
  reindexes) any held Going seat first, since Maybe is never a
  stronger commitment than Going. cancel_rsvp() handles both.
- mark_converted() carries the delivery ref forward to the newly
  created weekly successor row (tracked via a new
  predecessor_event_id column) in the same transaction, and clears it
  on the now-CONVERTED row — this is what lets the RSVP card follow
  the series instead of accumulating. New find_successor() and
  list_all_upcoming() (cross-guild, for the reconciliation sweep).

New utils/schedule_rsvp.py (ScheduleRsvpService) and
utils/schedule_views.py (ScheduleRsvpView) mirror the existing
PartyLobbyService/RecruitingCardView pattern: injected Discord-UI-
agnostic collaborators, embed-footer-based event_id routing, and the
same ensure/refresh-card idioms already established for the queue
card. schedule_commands.py wires auto-publish into confirm, card
refresh into rsvp/unrsvp, rollover-or-terminal-refresh into
open-scheduled, and the new session-refresh command.
ScheduleLifecycle gained on_startup and extended on_cleanup to sweep
list_all_upcoming() through the same reconcile() path used by
recovery — rsvp_service is optional so pre-existing reminder-only
callers/tests are unaffected.

39 new/updated tests across test_schedule_rsvp_cards.py (new),
test_party_schedule.py, and test_party_schedule_commands_characterization.py.
Full suite: 689 passing.

Version bumped to 2.3.0-rc.5 per RELEASE_PROCESS.md.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4c4e3bade6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread utils/schedule_rsvp.py Outdated
Comment thread utils/schedule_rsvp.py Outdated
Comment thread utils/party_schedule.py Outdated
- ensure_rsvp_card's publish call (channel.send) was unguarded, so a
  Forbidden/HTTPException there would propagate out of the shared
  periodic sweep and abort reconciliation for every other guild (and
  reminder delivery) behind it. Now wrapped best-effort like every
  other public-card post.
- The same method's repost-detection treated Forbidden/HTTPException
  as "message is gone" alongside NotFound, so a persistent permission
  gap would repost a brand-new duplicate card on every 5-minute sweep
  forever. Only a confirmed NotFound now triggers a repost; anything
  else leaves the delivery ref untouched for the next sweep to retry.
- rsvp()'s already-Going branch no-op-returned instead of applying the
  new role/fill/captain values, silently discarding the "Change Roles"
  card action even though the player received a success ack. Fixing
  it also surfaced a second, independent bug: the early return sat
  inside the transaction's `with` block, so self.get() ran on a
  separate connection before the transaction had committed and read
  back stale data. Restructured so the read-back always happens after
  commit.

Added direct regression coverage for all three.
@diese-tech
diese-tech merged commit a009119 into main Aug 23, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add persistent RSVP cards and reconciliation for scheduled Party sessions

2 participants