Email Notifictions
CI / ci (push) Canceled after 0s

Core logic: types.ts (defaults), notification.service.ts (skipEmail + dedup), booking-notifications.ts, loyalty-notifications.ts, reminder-scheduling.ts, booking.service.ts + scheduling.service.ts (staff-selection capture), features/owner/actions/appointments.ts.
Schema: prisma/schema.prisma + new migration 20260912165903_add_staff_selected_by_customer.
UI: both notification-preferences pages (customer, staff) now use the same default logic as the actual send decision.
Tests: notification.service.integration.test.ts, booking-notifications.integration.test.ts, reminder-scheduling.integration.test.ts, loyalty-notifications.integration.test.ts, plus a new createTestAppointment fixture.
Docs: docs/notifications.md rewritten to match.

Default changes

Exactly as specified — full table is in docs/notifications.md. In short: STAFF_NEW_APPOINTMENT, STAFF_APPOINTMENT_RESCHEDULED, STAFF_APPOINTMENT_REMINDER, OWNER_NEW_BOOKING, OWNER_NO_SHOW now default off; everything else that was already toggleable is untouched.

No longer email (fixed, not a toggle)

BOOKING_COMPLETED, LOYALTY_STAMP_ADDED, LOYALTY_REWARD_REDEEMED — always. BOOKING_BARBER_CHANGED — only when the customer originally booked "any available barber." The customer's 2-hour reminder is removed entirely (24h stays); staff keeps both, just off by default. All via a new skipEmail flag on notify(), checked in one place — no new preference rows for these.

Deduplication

One new EQUIVALENCE_GROUPS map in notification.service.ts (STAFF_NEW_APPOINTMENT>OWNER_NEW_BOOKING, STAFF_APPOINTMENT_CANCELLED>OWNER_BOOKING_CANCELLED). Inside notify(), before creating deliveries, it checks for an already-created equivalent notification for the same (businessId, appointmentId, recipientUserId); if found and at-least-as-relevant, the new one gets zero deliveries on every channel (row still created for audit trail). Only listed types participate, so OWNER_NO_SHOW and any future actionable owner type are never touched.
This commit is contained in:
Stefan Lazic committed 2026-09-12 18:27:11 +01:00
1 parent 7224c87cef
commit 4573587b78
18 files changed
+999 -172

No files matched your search

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE `Appointment` ADD COLUMN `staffSelectedByCustomer` BOOLEAN NOT NULL DEFAULT true;
+12
View File
@@ -1124,6 +1124,18 @@ model Appointment {
staffId String
staff StaffProfile @relation(fields: [staffId], references: [id], onDelete: Restrict)
/// Whether the customer picked THIS specific staff member when they
/// originally booked, as opposed to "any available barber" (the system
/// then auto-assigns one — see BookingService.createBooking). Set once
/// at creation and never rewritten by a later reassignment — it
/// describes the ORIGINAL booking, used to decide whether a later
/// staff reassignment is worth emailing the customer about (see
/// notifyBookingBarberChanged): someone who never expressed a
/// preference is less likely to care who they end up with. Defaults to
/// true so every appointment created before this field existed keeps
/// notifying on reassignment exactly as it did before.
staffSelectedByCustomer Boolean @default(true)
status AppointmentStatus @default(CONFIRMED)
/// Stored as UTC instants; the business's `timezone` (see Business model)