fix(gro-38): prod/demo auth and API-based seed (#117)
Closes GRO-38. Adds POST /api/admin/seed (manager-only, gated by SEED_KNOWN_USERS_ONLY) and separates dev vs prod seeding paths. Reviewed and approved by CTO and QA. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit was merged in pull request #117.
This commit is contained in:
committed by
GitHub
parent
d0b4baf5aa
commit
e3220af9ce
@@ -149,3 +149,55 @@ ${actionHtml}
|
||||
<p>— Groom Book</p>`,
|
||||
};
|
||||
}
|
||||
|
||||
interface WaitlistNotificationData {
|
||||
clientName: string;
|
||||
petName: string;
|
||||
serviceName: string;
|
||||
preferredDate: string;
|
||||
preferredTime: string;
|
||||
}
|
||||
|
||||
export function buildWaitlistNotificationEmail(
|
||||
to: string,
|
||||
data: WaitlistNotificationData
|
||||
): Mail.Options {
|
||||
const apiUrl = process.env.API_URL ?? "http://localhost:3000";
|
||||
const bookUrl = `${apiUrl}/book`;
|
||||
return {
|
||||
to,
|
||||
subject: `Appointment Cancelled — A slot has opened up for ${data.petName}`,
|
||||
text: [
|
||||
`Hi ${data.clientName},`,
|
||||
``,
|
||||
`Great news! An appointment slot has become available.`,
|
||||
``,
|
||||
`We had a cancellation for:`,
|
||||
` Pet: ${data.petName}`,
|
||||
` Service: ${data.serviceName}`,
|
||||
` Date: ${data.preferredDate}`,
|
||||
` Time: ${data.preferredTime}`,
|
||||
``,
|
||||
`If you're still interested, book now before this slot is taken!`,
|
||||
``,
|
||||
`Book your appointment: ${bookUrl}`,
|
||||
``,
|
||||
`— Groom Book`,
|
||||
].join("\n"),
|
||||
html: `
|
||||
<p>Hi ${data.clientName},</p>
|
||||
<p>Great news! <strong>An appointment slot has become available</strong>.</p>
|
||||
<p>We had a cancellation for:</p>
|
||||
<table style="border-collapse:collapse;margin:1em 0">
|
||||
<tr><td style="padding:4px 12px 4px 0;font-weight:600;color:#6b7280">Pet</td><td>${data.petName}</td></tr>
|
||||
<tr><td style="padding:4px 12px 4px 0;font-weight:600;color:#6b7280">Service</td><td>${data.serviceName}</td></tr>
|
||||
<tr><td style="padding:4px 12px 4px 0;font-weight:600;color:#6b7280">Date</td><td>${data.preferredDate}</td></tr>
|
||||
<tr><td style="padding:4px 12px 4px 0;font-weight:600;color:#6b7280">Time</td><td>${data.preferredTime}</td></tr>
|
||||
</table>
|
||||
<div style="margin:1.5em 0">
|
||||
<a href="${bookUrl}" style="display:inline-block;padding:12px 24px;background:#10b981;color:#fff;text-decoration:none;border-radius:6px;font-weight:600;font-size:16px">Book This Slot</a>
|
||||
</div>
|
||||
<p>If you're no longer interested, you can ignore this email or remove yourself from the waitlist in your portal.</p>
|
||||
<p>— Groom Book</p>`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { and, eq, getDb, waitlistEntries, clients, pets, services } from "@groombook/db";
|
||||
import { buildWaitlistNotificationEmail, sendEmail } from "./email.js";
|
||||
|
||||
export async function notifyWaitlistForAppointment(
|
||||
appointmentId: string,
|
||||
appointmentDate: string,
|
||||
appointmentTime: string,
|
||||
serviceId: string
|
||||
): Promise<void> {
|
||||
const db = getDb();
|
||||
|
||||
const matchingEntries = await db
|
||||
.select()
|
||||
.from(waitlistEntries)
|
||||
.where(
|
||||
and(
|
||||
eq(waitlistEntries.preferredDate, appointmentDate),
|
||||
eq(waitlistEntries.preferredTime, appointmentTime),
|
||||
eq(waitlistEntries.serviceId, serviceId),
|
||||
eq(waitlistEntries.status, "active")
|
||||
)
|
||||
);
|
||||
|
||||
for (const entry of matchingEntries) {
|
||||
const [client] = await db
|
||||
.select({ name: clients.name, email: clients.email, emailOptOut: clients.emailOptOut })
|
||||
.from(clients)
|
||||
.where(eq(clients.id, entry.clientId))
|
||||
.limit(1);
|
||||
|
||||
if (!client?.email || client.emailOptOut) continue;
|
||||
|
||||
const [pet] = await db
|
||||
.select({ name: pets.name })
|
||||
.from(pets)
|
||||
.where(eq(pets.id, entry.petId))
|
||||
.limit(1);
|
||||
|
||||
const [service] = await db
|
||||
.select({ name: services.name })
|
||||
.from(services)
|
||||
.where(eq(services.id, entry.serviceId))
|
||||
.limit(1);
|
||||
|
||||
if (!pet || !service) continue;
|
||||
|
||||
const email = buildWaitlistNotificationEmail(client.email, {
|
||||
clientName: client.name,
|
||||
petName: pet.name,
|
||||
serviceName: service.name,
|
||||
preferredDate: appointmentDate,
|
||||
preferredTime: appointmentTime,
|
||||
});
|
||||
|
||||
const sent = await sendEmail(email);
|
||||
if (sent) {
|
||||
await db
|
||||
.update(waitlistEntries)
|
||||
.set({ status: "notified", notifiedAt: new Date(), updatedAt: new Date() })
|
||||
.where(eq(waitlistEntries.id, entry.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user