fix(lifecycle): OAuth pw 500, invite lockout, reminder re-arm, partial-unique
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 38s
CI & Build / Python tests (push) Successful in 57s
CI & Build / Build & push image (push) Has been cancelled

Drift-audit Group 8 (lifecycle gaps):

- change_password no longer 500s for OAuth-only users: short-circuit when
  password_hash is None (verify_password would crash on None) so the route
  returns a clean 4xx instead of a 500.
- register_with_invitation no longer locks the invitee out on a username
  collision: create the user FIRST, then mark the token used, so a failed
  creation (409) leaves the single-use invite valid for retry.
- update_event re-arms reminder_sent_at when start_dt/reminder_minutes change,
  so a rescheduled event fires again instead of being permanently suppressed.
- Migration 0061: uq_topic_per_rulebook / uq_rule_per_topic become PARTIAL
  unique indexes (WHERE deleted_at IS NULL). Trashing 'X' then recreating it
  no longer 500s on the dead row's title. Model __table_args__ updated to match.

Deferred: per-occurrence reminders for recurring events (event_scheduler) —
needs a per-occurrence reminder-state design, not a one-line gate tweak.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 19:23:35 -04:00
co-authored by Claude Opus 4.8
parent 2fd9a2300a
commit 7ce5bb8450
4 changed files with 88 additions and 8 deletions
+5
View File
@@ -331,6 +331,11 @@ async def update_event(user_id: int, event_id: int, **fields) -> Event | None:
for key, value in fields.items():
if key in allowed and (value is not None or key in nullable):
setattr(event, key, value)
# Re-arm the reminder when the timing changes, so an event moved to a
# new (future) time — or given a new lead time — fires again instead of
# being permanently suppressed by a stale reminder_sent_at.
if "start_dt" in fields or "reminder_minutes" in fields:
event.reminder_sent_at = None
await session.commit()
await session.refresh(event)