feat: appointment confirmation and cancellation (GH #98, GRO-153)
Add customer confirmation/cancellation flow for appointments: - DB migration (0013): add confirmation_status, confirmed_at, cancelled_at, confirmation_token to appointments table with index on token column - schema.ts + factories.ts + types: expose new columns and ConfirmationStatus type - GET /api/book/confirm/:token — tokenized confirm via email link (redirects) - GET /api/book/cancel/:token — tokenized cancel via email link, single-use token - POST /api/appointments/:id/confirm — portal/staff confirm endpoint - POST /api/appointments/:id/cancel — portal/staff cancel endpoint - Reminder emails now include Confirm/Cancel CTA buttons with tokenized links - Reminder service generates confirmation token if missing before sending - Staff calendar shows confirmation status indicator on appointment cards and in the detail modal (confirmed ✓ / customer cancelled ✗) - /booking/confirmed, /booking/cancelled, /booking/error redirect pages - 23 new unit tests covering all new endpoints and edge cases Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import cron from "node-cron";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import {
|
||||
and,
|
||||
eq,
|
||||
@@ -51,6 +52,7 @@ export async function runReminderCheck(): Promise<void> {
|
||||
serviceId: appointments.serviceId,
|
||||
staffId: appointments.staffId,
|
||||
status: appointments.status,
|
||||
confirmationToken: appointments.confirmationToken,
|
||||
})
|
||||
.from(appointments)
|
||||
.where(
|
||||
@@ -109,6 +111,17 @@ export async function runReminderCheck(): Promise<void> {
|
||||
|
||||
if (!pet || !service) continue;
|
||||
|
||||
// Ensure the appointment has a confirmation token before sending the reminder.
|
||||
// Generate one if it doesn't have one yet (e.g. pre-existing appointments).
|
||||
let confirmationToken = appt.confirmationToken;
|
||||
if (!confirmationToken) {
|
||||
confirmationToken = randomBytes(32).toString("hex");
|
||||
await db
|
||||
.update(appointments)
|
||||
.set({ confirmationToken, updatedAt: new Date() })
|
||||
.where(eq(appointments.id, appt.id));
|
||||
}
|
||||
|
||||
const sent = await sendEmail(
|
||||
buildReminderEmail(
|
||||
client.email,
|
||||
@@ -119,7 +132,8 @@ export async function runReminderCheck(): Promise<void> {
|
||||
groomerName,
|
||||
startTime: appt.startTime,
|
||||
},
|
||||
window.hours
|
||||
window.hours,
|
||||
confirmationToken
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user