|
| 1 | +import "server-only"; |
| 2 | + |
| 3 | +import { asc, eq } from "drizzle-orm"; |
| 4 | +import { pgSchema, uuid, text } from "drizzle-orm/pg-core"; |
| 5 | + |
| 6 | +import { db } from "@/lib/db"; |
| 7 | +import { |
| 8 | + children, |
| 9 | + coachingSessions, |
| 10 | + emergencyContacts, |
| 11 | + formQuestionAnswers, |
| 12 | + formQuestions, |
| 13 | + profiles, |
| 14 | +} from "@/lib/db/schema"; |
| 15 | +import { getService } from "@/app/(authenticated)/services/queries"; |
| 16 | +import { sendEmail } from "@/lib/email/client"; |
| 17 | +import { coordinatorBookingEmail, type ChildInfo } from "@/lib/email/templates"; |
| 18 | +import { userHasActiveSubscription } from "@/lib/stripe"; |
| 19 | +import type { TimeSlot } from "@/lib/scheduling/time-slot"; |
| 20 | + |
| 21 | +const authSchema = pgSchema("auth"); |
| 22 | +const authUsers = authSchema.table("users", { |
| 23 | + id: uuid("id").primaryKey(), |
| 24 | + email: text("email"), |
| 25 | +}); |
| 26 | + |
| 27 | +type ProfileWithEmail = { |
| 28 | + firstName: string; |
| 29 | + lastName: string; |
| 30 | + email: string | null; |
| 31 | + address: string | null; |
| 32 | + gender: string | null; |
| 33 | + dob: string | null; |
| 34 | + phone: string | null; |
| 35 | +}; |
| 36 | + |
| 37 | +async function getProfileWithEmail( |
| 38 | + profileId: string, |
| 39 | +): Promise<ProfileWithEmail | null> { |
| 40 | + const [row] = await db |
| 41 | + .select({ |
| 42 | + firstName: profiles.firstName, |
| 43 | + lastName: profiles.lastName, |
| 44 | + email: authUsers.email, |
| 45 | + address: profiles.address, |
| 46 | + gender: profiles.gender, |
| 47 | + dob: profiles.dob, |
| 48 | + phone: profiles.phone, |
| 49 | + }) |
| 50 | + .from(profiles) |
| 51 | + .innerJoin(authUsers, eq(authUsers.id, profiles.id)) |
| 52 | + .where(eq(profiles.id, profileId)) |
| 53 | + .limit(1); |
| 54 | + return row ?? null; |
| 55 | +} |
| 56 | + |
| 57 | +async function loadChildInfo(childId: string): Promise<ChildInfo | null> { |
| 58 | + const [child] = await db |
| 59 | + .select() |
| 60 | + .from(children) |
| 61 | + .where(eq(children.id, childId)) |
| 62 | + .limit(1); |
| 63 | + if (!child) return null; |
| 64 | + |
| 65 | + const contacts = await db |
| 66 | + .select({ |
| 67 | + fullName: emergencyContacts.fullName, |
| 68 | + relationship: emergencyContacts.relationship, |
| 69 | + phoneNumber: emergencyContacts.phoneNumber, |
| 70 | + emailAddress: emergencyContacts.emailAddress, |
| 71 | + }) |
| 72 | + .from(emergencyContacts) |
| 73 | + .where(eq(emergencyContacts.childId, childId)); |
| 74 | + |
| 75 | + const answers = await db |
| 76 | + .select({ |
| 77 | + prompt: formQuestions.prompt, |
| 78 | + answer: formQuestionAnswers.answer, |
| 79 | + sortOrder: formQuestions.sortOrder, |
| 80 | + }) |
| 81 | + .from(formQuestionAnswers) |
| 82 | + .innerJoin( |
| 83 | + formQuestions, |
| 84 | + eq(formQuestions.id, formQuestionAnswers.formQuestionId), |
| 85 | + ) |
| 86 | + .where(eq(formQuestionAnswers.childId, childId)) |
| 87 | + .orderBy(asc(formQuestions.sortOrder)); |
| 88 | + |
| 89 | + return { |
| 90 | + firstName: child.firstName, |
| 91 | + lastName: child.lastName, |
| 92 | + dob: child.dob, |
| 93 | + gender: child.gender, |
| 94 | + allergies: child.allergies, |
| 95 | + medicalConditions: child.medicalConditions, |
| 96 | + medications: child.medications, |
| 97 | + emergencyContacts: contacts, |
| 98 | + formAnswers: answers.map((a) => ({ prompt: a.prompt, answer: a.answer })), |
| 99 | + }; |
| 100 | +} |
| 101 | + |
| 102 | +export async function sendCoordinatorBookingEmail( |
| 103 | + sessionId: string, |
| 104 | +): Promise<void> { |
| 105 | + try { |
| 106 | + const [session] = await db |
| 107 | + .select() |
| 108 | + .from(coachingSessions) |
| 109 | + .where(eq(coachingSessions.id, sessionId)) |
| 110 | + .limit(1); |
| 111 | + if (!session) { |
| 112 | + console.error( |
| 113 | + "[sendCoordinatorBookingEmail] session missing", |
| 114 | + sessionId, |
| 115 | + ); |
| 116 | + return; |
| 117 | + } |
| 118 | + |
| 119 | + const [coordinator, client, service, hasActiveSubscription] = |
| 120 | + await Promise.all([ |
| 121 | + getProfileWithEmail(session.coordinatorId), |
| 122 | + getProfileWithEmail(session.userId), |
| 123 | + getService(session.serviceId), |
| 124 | + userHasActiveSubscription(session.userId), |
| 125 | + ]); |
| 126 | + |
| 127 | + if (!coordinator?.email) { |
| 128 | + console.error( |
| 129 | + "[sendCoordinatorBookingEmail] coordinator email missing", |
| 130 | + sessionId, |
| 131 | + ); |
| 132 | + return; |
| 133 | + } |
| 134 | + if (!client) { |
| 135 | + console.error( |
| 136 | + "[sendCoordinatorBookingEmail] client profile missing", |
| 137 | + sessionId, |
| 138 | + ); |
| 139 | + return; |
| 140 | + } |
| 141 | + |
| 142 | + const serviceTitle = service?.title ?? "Private lesson"; |
| 143 | + const durationMinutes = service?.durationMinutes ?? 60; |
| 144 | + |
| 145 | + let scheduledSlot: TimeSlot | null = null; |
| 146 | + if (session.scheduledAt) { |
| 147 | + const start = session.scheduledAt; |
| 148 | + const end = new Date(start.getTime() + durationMinutes * 60_000); |
| 149 | + scheduledSlot = { start: start.toISOString(), end: end.toISOString() }; |
| 150 | + } |
| 151 | + |
| 152 | + const child = session.childId |
| 153 | + ? await loadChildInfo(session.childId) |
| 154 | + : null; |
| 155 | + |
| 156 | + const email = await coordinatorBookingEmail({ |
| 157 | + coordinatorName: |
| 158 | + `${coordinator.firstName} ${coordinator.lastName}`.trim(), |
| 159 | + serviceTitle, |
| 160 | + client: { |
| 161 | + firstName: client.firstName, |
| 162 | + lastName: client.lastName, |
| 163 | + email: client.email ?? "—", |
| 164 | + address: client.address, |
| 165 | + gender: client.gender, |
| 166 | + dob: client.dob, |
| 167 | + phone: client.phone, |
| 168 | + hasActiveSubscription, |
| 169 | + }, |
| 170 | + scheduledSlot, |
| 171 | + requestedAvailability: |
| 172 | + (session.selectedTimeSlots as TimeSlot[] | null) ?? [], |
| 173 | + notes: session.notes, |
| 174 | + child, |
| 175 | + }); |
| 176 | + |
| 177 | + await sendEmail({ to: coordinator.email, ...email }); |
| 178 | + } catch (e) { |
| 179 | + console.error("[sendCoordinatorBookingEmail]", e); |
| 180 | + } |
| 181 | +} |
0 commit comments