fixed button layout on appointment details page. added an explainer for all the notifications
This commit is contained in:
3 files changed
+175
-26
No files matched your search
@@ -0,0 +1,154 @@
|
||||
# Notifications reference
|
||||
|
||||
A living reference for every notification NextChair sends — what it is,
|
||||
who gets it, on which channels, and whether the recipient can turn it
|
||||
off. Unlike the other files in `docs/` (dated phase summaries), this one
|
||||
is meant to be kept current as notifications change; if you add, remove,
|
||||
or retarget one, update this file in the same change.
|
||||
|
||||
## Two separate mechanisms
|
||||
|
||||
Most of what's on this page goes through the **queued notification
|
||||
system**: an event happens, a `Notification` + one `NotificationDelivery`
|
||||
row per applicable channel is created immediately, but the actual
|
||||
EMAIL/PUSH send only happens when the background processor runs (see
|
||||
`src/services/notifications/reminder-worker.ts`, triggered by
|
||||
`POST /api/cron/process-notifications` — see the root README's
|
||||
"Notification delivery" section for how that's scheduled on a real
|
||||
deployment). `IN_APP` deliveries have no send step; they're just a
|
||||
database row the notification bell/centre reads directly.
|
||||
|
||||
**One-time codes** (below, in their own section) are different — they're
|
||||
sent synchronously, the moment they're requested, with no queue at all.
|
||||
|
||||
Either way, nothing actually reaches an inbox unless that business has
|
||||
`EMAIL_PROVIDER=brevo` configured (Manager's Settings → Email, or a
|
||||
business's own `.env`) — with the default `EMAIL_PROVIDER=console`,
|
||||
every send just logs to the container's console instead.
|
||||
|
||||
## Channels
|
||||
|
||||
- **EMAIL** — sent only if the recipient has a resolvable email address
|
||||
(a registered user's `User.email`, or a guest customer's
|
||||
`CustomerProfile.email`).
|
||||
- **PUSH** — sent only if the recipient has at least one active (not
|
||||
revoked) browser push subscription.
|
||||
- **IN_APP** — only meaningful for a recipient with a real account
|
||||
(`recipientUserId` set) — a guest customer never gets one, since they
|
||||
have nothing to sign into.
|
||||
|
||||
## Preferences — who can turn what off
|
||||
|
||||
See `src/services/notifications/types.ts`'s `PREFERENCE_CONTROLLED_TYPES`
|
||||
— this is the single source of truth; everything not listed there is
|
||||
**required** (always sent on every channel it has a valid target for, no
|
||||
toggle anywhere). Currently:
|
||||
|
||||
- **Customer-facing**: only `BOOKING_REMINDER` is optional. Everything
|
||||
else (confirmations, changes, cancellations, loyalty) always notifies
|
||||
the customer — not finding out your appointment was cancelled is a
|
||||
real risk, not just noise.
|
||||
- **Staff- and owner-facing**: every type is optional. The owner of a
|
||||
business is commonly also its only barber — one `StaffProfile`, one
|
||||
account — so without this, a single new booking would unconditionally
|
||||
notify that one person twice (`STAFF_NEW_APPOINTMENT` +
|
||||
`OWNER_NEW_BOOKING`) on every channel, including in-app, with no way to
|
||||
quiet either copy.
|
||||
|
||||
Toggles live at **Settings → Notifications** — customers see one
|
||||
"Appointment reminders" group; staff see their own six groups, plus five
|
||||
more owner-only groups if they hold `VIEW_REPORTING` (i.e. are actually
|
||||
an owner — see `src/services/notifications/recipients.ts`'s
|
||||
`getOwnerUserIds`, which checks the same permission). A preference with
|
||||
no saved row yet defaults to **on** — turning something off is always an
|
||||
explicit, deliberate action.
|
||||
|
||||
---
|
||||
|
||||
## Customer-facing
|
||||
|
||||
| Type | Fires when | Default subject |
|
||||
|---|---|---|
|
||||
| `BOOKING_CONFIRMED` | A new booking is created and doesn't need approval | "Booking confirmed" |
|
||||
| `BOOKING_PENDING_APPROVAL` | A new booking is created but the business requires approval first | "Booking request received" |
|
||||
| `BOOKING_APPROVED` | An owner approves a pending booking | "Booking approved" |
|
||||
| `BOOKING_RESCHEDULED` | The appointment's time changes (including an owner reassigning staff, which also triggers a reschedule) | "Booking rescheduled" |
|
||||
| `BOOKING_CANCELLED` | The appointment is cancelled, by anyone | "Booking cancelled" (carries a "This was a late cancellation." note when applicable — not a separate type) |
|
||||
| `BOOKING_BARBER_CHANGED` | An owner reassigns the appointment to a different staff member | "Your barber has changed" |
|
||||
| `BOOKING_COMPLETED` | The appointment is marked completed | "Thanks for visiting" |
|
||||
| `BOOKING_REMINDER` *(optional)* | 24 hours and 2 hours before a confirmed appointment (each is its own send) | "Upcoming appointment reminder" |
|
||||
| `LOYALTY_STAMP_ADDED` | A loyalty stamp is added to their card | "You've earned a loyalty stamp" |
|
||||
| `LOYALTY_REWARD_EARNED` | Their stamp count reaches the reward threshold | "You've earned a reward" |
|
||||
| `LOYALTY_REWARD_EXPIRING` | A daily sweep finds their reward is nearing its expiry date (fires once per calendar day it's still found, not once total) | "Your reward is expiring soon" |
|
||||
| `LOYALTY_REWARD_REDEEMED` | Their reward is applied to an appointment | "Reward applied" |
|
||||
|
||||
## Staff-facing
|
||||
|
||||
| Type | Fires when | Default subject |
|
||||
|---|---|---|
|
||||
| `STAFF_NEW_APPOINTMENT` *(optional)* | A new appointment is booked with them | "New appointment" |
|
||||
| `STAFF_APPOINTMENT_RESCHEDULED` *(optional)* | One of their appointments is rescheduled | "Appointment rescheduled" |
|
||||
| `STAFF_APPOINTMENT_CANCELLED` *(optional)* | One of their appointments is cancelled | "Appointment cancelled" |
|
||||
| `STAFF_APPOINTMENT_REMINDER` *(optional)* | 24 hours and 2 hours before their appointment | "Upcoming appointment reminder" |
|
||||
| `STAFF_SCHEDULE_CHANGED` *(optional)* | An **owner** edits their working hours/breaks/time-off on their behalf — never fires from the self-service editors, since notifying someone about their own edit would just be noise | "Your schedule was updated" |
|
||||
| `STAFF_INVITATION_RECEIVED` *(optional)* | They're added as a new team member | "You've been invited to join `{business}`" |
|
||||
| `STAFF_APPOINTMENT_CHECKED_IN` | Defined in the schema, has a recipient assigned — **but nothing in the codebase currently triggers it.** Preference-controlled for forward-compatibility, but has no toggle in the UI since there's nothing to preview | — |
|
||||
|
||||
## Owner-facing
|
||||
|
||||
Sent to *every* active staff member whose role holds `VIEW_REPORTING` —
|
||||
that can be more than one person; there's no single "the owner" column
|
||||
anywhere in the schema.
|
||||
|
||||
| Type | Fires when | Default subject |
|
||||
|---|---|---|
|
||||
| `OWNER_NEW_BOOKING` *(optional)* | A new booking is made anywhere in the business | "New booking" |
|
||||
| `OWNER_BOOKING_CANCELLED` *(optional)* | A booking is cancelled anywhere in the business | "Booking cancelled" |
|
||||
| `OWNER_NO_SHOW` *(optional)* | A customer is marked as a no-show (staff-facing equivalent doesn't exist — the barber who marked it already knows) | "No-show recorded" |
|
||||
| `OWNER_STAFF_INVITATION_ACCEPTED` *(optional)* | An invited staff member verifies their email and joins | "Invitation accepted" |
|
||||
| `OWNER_REPEATED_DELIVERY_FAILURE` *(optional)* | The background processor itself detects repeated delivery failures that day — a self-monitoring alert, not triggered by a domain event. At most once per business per day | "Some notifications failed to send" |
|
||||
|
||||
---
|
||||
|
||||
## One-time codes (not part of the system above)
|
||||
|
||||
Sent immediately and synchronously — see `src/lib/auth/otp.service.ts`.
|
||||
Not queued, not preference-controlled, no in-app record.
|
||||
|
||||
| Purpose | Sent to | When |
|
||||
|---|---|---|
|
||||
| `login` | The email being signed in with | Every sign-in attempt |
|
||||
| `register` | The email registering | Customer self-registration |
|
||||
| `staff-invite` | The invited email | Accepting a staff invitation (`/invite/accept`) |
|
||||
| `install` | The administrator's email | The install wizard's administrator-verification step |
|
||||
| `email-change` | The **new** email address | A signed-in user changing their account email |
|
||||
|
||||
## Platform-level (Manager, not tied to any business)
|
||||
|
||||
- **Settings → Email → Send Test Email** — a manual, one-off admin action
|
||||
in the Manager app, unrelated to any business's own notification queue.
|
||||
See `manager/src/lib/test-email.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Where things live, if you're changing any of this
|
||||
|
||||
- **What exists / who it's for**: `prisma/schema.prisma`'s
|
||||
`NotificationType` enum, and `RECIPIENT_KIND` in
|
||||
`src/services/notifications/types.ts`.
|
||||
- **Whether it's optional**: `PREFERENCE_CONTROLLED_TYPES`, same file.
|
||||
- **When it actually fires**: `src/services/scheduling/booking-notifications.ts`,
|
||||
`reminder-scheduling.ts`, `src/services/loyalty/loyalty-notifications.ts`,
|
||||
`src/services/staff/staff-notifications.ts`,
|
||||
`src/services/staff/staff-invitation.service.ts`, and
|
||||
`reminder-worker.ts` (for the self-monitoring alert).
|
||||
- **Channel resolution / preference enforcement**:
|
||||
`src/services/notifications/notification.service.ts`'s
|
||||
`resolveChannels`/`isChannelEnabled`.
|
||||
- **The actual send**: `src/services/notifications/providers/` (Brevo/
|
||||
console email, web-push/console push) plus `reminder-worker.ts`, which
|
||||
processes the queue.
|
||||
- **Settings UI**: `src/features/staff/components/notification-preferences-form.tsx`
|
||||
(shared by customer/staff/owner), wired up per audience in
|
||||
`src/app/(customer)/settings/notifications/page.tsx` and
|
||||
`src/app/(staff)/staff/settings/notifications/page.tsx`.
|
||||
Reference in new issue
Block a user