Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f97a19cdd | |||
| a407f866d5 | |||
| 04147f3e6c |
@@ -0,0 +1,27 @@
|
|||||||
|
# The current version of the config schema
|
||||||
|
version: 1
|
||||||
|
# What protocol to use when performing git operations. Supported values: ssh, https
|
||||||
|
git_protocol: https
|
||||||
|
# What editor gh should run when creating issues, pull requests, etc. If blank, will refer to environment.
|
||||||
|
editor:
|
||||||
|
# When to interactively prompt. This is a global config that cannot be overridden by hostname. Supported values: enabled, disabled
|
||||||
|
prompt: enabled
|
||||||
|
# Preference for editor-based interactive prompting. This is a global config that cannot be overridden by hostname. Supported values: enabled, disabled
|
||||||
|
prefer_editor_prompt: disabled
|
||||||
|
# A pager program to send command output to, e.g. "less". If blank, will refer to environment. Set the value to "cat" to disable the pager.
|
||||||
|
pager:
|
||||||
|
# Aliases allow you to create nicknames for gh commands
|
||||||
|
aliases:
|
||||||
|
co: pr checkout
|
||||||
|
# The path to a unix socket through which to send HTTP connections. If blank, HTTP traffic will be handled by net/http.DefaultTransport.
|
||||||
|
http_unix_socket:
|
||||||
|
# What web browser gh should use when opening URLs. If blank, will refer to environment.
|
||||||
|
browser:
|
||||||
|
# Whether to display labels using their RGB hex color codes in terminals that support truecolor. Supported values: enabled, disabled
|
||||||
|
color_labels: disabled
|
||||||
|
# Whether customizable, 4-bit accessible colors should be used. Supported values: enabled, disabled
|
||||||
|
accessible_colors: disabled
|
||||||
|
# Whether an accessible prompter should be used. Supported values: enabled, disabled
|
||||||
|
accessible_prompter: disabled
|
||||||
|
# Whether to use a animated spinner as a progress indicator. If disabled, a textual progress indicator is used instead. Supported values: enabled, disabled
|
||||||
|
spinner: enabled
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
github.com:
|
||||||
|
users:
|
||||||
|
groombook-engineer[bot]:
|
||||||
|
oauth_token: ghs_znRlNnhuSsNZp0GejabxpkSUqXC9vt27yl3K
|
||||||
|
user: groombook-engineer[bot]
|
||||||
|
oauth_token: ghs_znRlNnhuSsNZp0GejabxpkSUqXC9vt27yl3K
|
||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
eq,
|
eq,
|
||||||
getDb,
|
getDb,
|
||||||
gte,
|
gte,
|
||||||
|
inArray,
|
||||||
lt,
|
lt,
|
||||||
appointments,
|
appointments,
|
||||||
clients,
|
clients,
|
||||||
@@ -64,56 +65,66 @@ export async function runReminderCheck(): Promise<void> {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const appt of upcoming) {
|
const appointmentIds: string[] = upcoming.map((a) => a.id as string);
|
||||||
// Check if reminder already sent (unique constraint prevents double-send)
|
|
||||||
const existing = await db
|
if (appointmentIds.length === 0) continue;
|
||||||
.select({ id: reminderLogs.id })
|
|
||||||
.from(reminderLogs)
|
const sentAppointmentIds = new Set(
|
||||||
.where(
|
(
|
||||||
and(
|
await db
|
||||||
eq(reminderLogs.appointmentId, appt.id),
|
.select({ appointmentId: reminderLogs.appointmentId })
|
||||||
eq(reminderLogs.reminderType, window.label)
|
.from(reminderLogs)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(reminderLogs.reminderType, window.label),
|
||||||
|
inArray(reminderLogs.appointmentId, appointmentIds)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
).map((r) => r.appointmentId)
|
||||||
|
);
|
||||||
|
|
||||||
|
const joinedRows = await db
|
||||||
|
.select({
|
||||||
|
appointmentId: appointments.id,
|
||||||
|
startTime: appointments.startTime,
|
||||||
|
clientId: appointments.clientId,
|
||||||
|
petId: appointments.petId,
|
||||||
|
serviceId: appointments.serviceId,
|
||||||
|
staffId: appointments.staffId,
|
||||||
|
confirmationToken: appointments.confirmationToken,
|
||||||
|
clientName: clients.name,
|
||||||
|
clientEmail: clients.email,
|
||||||
|
clientEmailOptOut: clients.emailOptOut,
|
||||||
|
petName: pets.name,
|
||||||
|
serviceName: services.name,
|
||||||
|
staffName: staff.name,
|
||||||
|
})
|
||||||
|
.from(appointments)
|
||||||
|
.innerJoin(clients, eq(appointments.clientId, clients.id))
|
||||||
|
.innerJoin(pets, eq(appointments.petId, pets.id))
|
||||||
|
.innerJoin(services, eq(appointments.serviceId, services.id))
|
||||||
|
.leftJoin(staff, eq(appointments.staffId, staff.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
gte(appointments.startTime, windowStart),
|
||||||
|
lt(appointments.startTime, windowEnd),
|
||||||
|
eq(appointments.status, "scheduled")
|
||||||
)
|
)
|
||||||
.limit(1);
|
);
|
||||||
|
|
||||||
if (existing.length > 0) continue; // already sent
|
const appointmentMap = new Map<string, typeof joinedRows[number]>();
|
||||||
|
for (const row of joinedRows) {
|
||||||
|
appointmentMap.set(row.appointmentId, row);
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch related records for the email
|
for (const appt of upcoming) {
|
||||||
const [client] = await db
|
if (sentAppointmentIds.has(appt.id)) continue;
|
||||||
.select({ name: clients.name, email: clients.email, emailOptOut: clients.emailOptOut })
|
|
||||||
.from(clients)
|
|
||||||
.where(eq(clients.id, appt.clientId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!client || !client.email || client.emailOptOut) continue;
|
const row = appointmentMap.get(appt.id);
|
||||||
|
if (!row) continue;
|
||||||
|
if (!row.clientEmail || row.clientEmailOptOut) continue;
|
||||||
|
if (!row.petName || !row.serviceName) continue;
|
||||||
|
|
||||||
const [pet] = await db
|
|
||||||
.select({ name: pets.name })
|
|
||||||
.from(pets)
|
|
||||||
.where(eq(pets.id, appt.petId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
const [service] = await db
|
|
||||||
.select({ name: services.name })
|
|
||||||
.from(services)
|
|
||||||
.where(eq(services.id, appt.serviceId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
let groomerName: string | null = null;
|
|
||||||
if (appt.staffId) {
|
|
||||||
const [groomer] = await db
|
|
||||||
.select({ name: staff.name })
|
|
||||||
.from(staff)
|
|
||||||
.where(eq(staff.id, appt.staffId))
|
|
||||||
.limit(1);
|
|
||||||
groomerName = groomer?.name ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
let confirmationToken = appt.confirmationToken;
|
||||||
if (!confirmationToken) {
|
if (!confirmationToken) {
|
||||||
confirmationToken = randomBytes(32).toString("hex");
|
confirmationToken = randomBytes(32).toString("hex");
|
||||||
@@ -125,12 +136,12 @@ export async function runReminderCheck(): Promise<void> {
|
|||||||
|
|
||||||
const sent = await sendEmail(
|
const sent = await sendEmail(
|
||||||
buildReminderEmail(
|
buildReminderEmail(
|
||||||
client.email,
|
row.clientEmail,
|
||||||
{
|
{
|
||||||
clientName: client.name,
|
clientName: row.clientName,
|
||||||
petName: pet.name,
|
petName: row.petName,
|
||||||
serviceName: service.name,
|
serviceName: row.serviceName,
|
||||||
groomerName,
|
groomerName: row.staffName ?? null,
|
||||||
startTime: appt.startTime,
|
startTime: appt.startTime,
|
||||||
},
|
},
|
||||||
window.hours,
|
window.hours,
|
||||||
@@ -139,7 +150,6 @@ export async function runReminderCheck(): Promise<void> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (sent) {
|
if (sent) {
|
||||||
// Record send — ignore conflicts (race condition between instances)
|
|
||||||
await db
|
await db
|
||||||
.insert(reminderLogs)
|
.insert(reminderLogs)
|
||||||
.values({ appointmentId: appt.id, reminderType: window.label })
|
.values({ appointmentId: appt.id, reminderType: window.label })
|
||||||
@@ -172,3 +182,4 @@ export async function runSessionCleanup(): Promise<void> {
|
|||||||
.delete(session)
|
.delete(session)
|
||||||
.where(lt(session.expiresAt, now));
|
.where(lt(session.expiresAt, now));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Submodule
+1
Submodule infra-repo added at ff42966751
Reference in New Issue
Block a user