notification_cursors table stores one read-cursor per user per tenant;
GET /v1/activity returns unreadCount + readAt, POST /v1/activity/read
persists the cursor. Reminder dispatches now append reminder.sent /
reminder.failed system events so delivery history reaches the feed.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Tenants paying for the SMS add-on still got email-only reminders —
the channel column existed but nothing armed or dispatched sms jobs.
Booking creation now adds a second job when SMS is enabled and the
customer left a phone; dispatch routes sms jobs through an injected
sender adapter wired to the existing sms.Service, so monthly limits,
usage metering, and Stripe billing apply unchanged. Lifecycle handling
from the previous milestone clears and re-arms both channels together.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The reminder pipeline fired regardless of what happened to the booking
after creation: cancelled customers got reminded about bookings that
weren't happening, reschedules kept the old reminder time, and
waitlisted bookings armed jobs too. Pending jobs are now cancelled on
both cancel paths, deleted and re-armed on reschedule, and dispatch
joins bookings.status to skip anything not confirmed. Reschedule also
gained the same availability-window and blocked-day validation as
booking creation, closing a 3am-reschedule hole via the manage URL.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>