"use server"; import { and, eq } from "drizzle-orm"; import { revalidatePath } from "next/cache"; import { z } from "zod"; import type { ApiAuthContext } from "@/lib/api-auth"; import { db } from "@/lib/db"; import { getCurrentSession } from "@/lib/session"; import { logActivityForScope } from "@/modules/_core/activity"; import { cancelReminder, listReminderOffsets } from "@/modules/_core/reminders"; import { petAppointments, petPrescriptions, pets, petVaccinations } from "../schema"; import { syncPetAppointmentReminders, syncPetDueDateReminder } from "./reminders"; import { appointmentInput, appointmentUpdateInput, petInput, petUpdateInput, prescriptionInput, prescriptionUpdateInput, vaccinationInput, vaccinationUpdateInput, } from "./schemas"; type Scope = Pick; const idInput = z.object({ id: z.string().uuid() }); async function requirePet(scope: Scope, id: string) { const [pet] = await db .select() .from(pets) .where(and(eq(pets.id, id), eq(pets.householdId, scope.householdId))) .limit(1); if (!pet) throw new Error("Pet not found"); return pet; } async function requireAppointment(scope: Scope, id: string) { const [row] = await db .select() .from(petAppointments) .where(and(eq(petAppointments.id, id), eq(petAppointments.householdId, scope.householdId))) .limit(1); if (!row) throw new Error("Record not found"); return row; } async function requireVaccination(scope: Scope, id: string) { const [row] = await db .select() .from(petVaccinations) .where(and(eq(petVaccinations.id, id), eq(petVaccinations.householdId, scope.householdId))) .limit(1); if (!row) throw new Error("Record not found"); return row; } async function requirePrescription(scope: Scope, id: string) { const [row] = await db .select() .from(petPrescriptions) .where(and(eq(petPrescriptions.id, id), eq(petPrescriptions.householdId, scope.householdId))) .limit(1); if (!row) throw new Error("Record not found"); return row; } function revalidatePet(id: string) { revalidatePath("/pets"); revalidatePath(`/pets/${id}`); } export async function createPetForScope(scope: Scope, input: z.input) { const parsed = petInput.parse(input); const [pet] = await db .insert(pets) .values({ householdId: scope.householdId, ...parsed, breed: parsed.breed ?? null, birthDate: parsed.birthDate ?? null, notes: parsed.notes ?? null, primaryImageUrl: parsed.primaryImageUrl ?? parsed.images[0] ?? null, }) .returning(); if (!pet) throw new Error("Pet was not created"); await logActivityForScope(scope, { entityType: "pets.pet", entityId: pet.id, action: "create", payload: { name: pet.name }, }); return pet; } export async function createPet(input: z.input) { const { household, user } = await getCurrentSession(); const pet = await createPetForScope({ householdId: household.id, userId: user.id }, input); revalidatePet(pet.id); return pet; } export async function updatePetForScope( scope: Scope, input: { id: string } & z.input, ) { const parsed = idInput.and(petUpdateInput).parse(input); await requirePet(scope, parsed.id); const { id, ...changes } = parsed; await db .update(pets) .set({ ...changes, updatedAt: new Date() }) .where(eq(pets.id, id)); await logActivityForScope(scope, { entityType: "pets.pet", entityId: id, action: "update" }); } export async function updatePet(input: { id: string } & z.input) { const { household, user } = await getCurrentSession(); await updatePetForScope({ householdId: household.id, userId: user.id }, input); revalidatePet(input.id); } export async function deletePetForScope(scope: Scope, input: { id: string }) { const { id } = idInput.parse(input); const pet = await requirePet(scope, id); await logActivityForScope(scope, { entityType: "pets.pet", entityId: id, action: "delete", payload: { name: pet.name }, }); await db.delete(pets).where(eq(pets.id, id)); } export async function deletePet(input: { id: string }) { const { household, user } = await getCurrentSession(); await deletePetForScope({ householdId: household.id, userId: user.id }, input); revalidatePath("/pets"); } export async function createAppointmentForScope( scope: Scope, input: z.input, ) { const parsed = appointmentInput.parse(input); const pet = await requirePet(scope, parsed.petId); const [row] = await db .insert(petAppointments) .values({ householdId: scope.householdId, petId: parsed.petId, title: parsed.title, appointmentAt: parsed.appointmentAt, clinic: parsed.clinic ?? null, notes: parsed.notes ?? null, }) .returning(); if (!row) throw new Error("Appointment was not created"); if (scope.userId) await syncPetAppointmentReminders({ householdId: scope.householdId, appointmentId: row.id, appointmentAt: row.appointmentAt, createdBy: scope.userId, petName: pet.name, title: row.title, offsets: parsed.reminderOffsets, }); return row; } export async function updateAppointmentForScope( scope: Scope, input: { id: string } & z.input, ) { const parsed = idInput.and(appointmentUpdateInput).parse(input); const row = await requireAppointment(scope, parsed.id); const pet = await requirePet(scope, row.petId); const offsets = parsed.reminderOffsets ?? (await listReminderOffsets("pets.appointment", row.id)); const { id, reminderOffsets: _, ...changes } = parsed; const [updated] = await db .update(petAppointments) .set({ ...changes, updatedAt: new Date() }) .where(eq(petAppointments.id, id)) .returning(); if (!updated) throw new Error("Appointment not found"); if (scope.userId) await syncPetAppointmentReminders({ householdId: scope.householdId, appointmentId: id, appointmentAt: updated.appointmentAt, createdBy: scope.userId, petName: pet.name, title: updated.title, offsets, }); return updated; } export async function deleteAppointmentForScope(scope: Scope, input: { id: string }) { const { id } = idInput.parse(input); await requireAppointment(scope, id); await cancelReminder("pets.appointment", id); await db.delete(petAppointments).where(eq(petAppointments.id, id)); } export async function createAppointment(input: z.input) { const { household, user } = await getCurrentSession(); const row = await createAppointmentForScope( { householdId: household.id, userId: user.id }, input, ); revalidatePet(row.petId); return row; } export async function updateAppointment( input: { id: string } & z.input, ) { const { household, user } = await getCurrentSession(); const row = await updateAppointmentForScope( { householdId: household.id, userId: user.id }, input, ); revalidatePet(row.petId); return row; } export async function deleteAppointment(input: { id: string }) { const { household, user } = await getCurrentSession(); const row = await requireAppointment({ householdId: household.id, userId: user.id }, input.id); await deleteAppointmentForScope({ householdId: household.id, userId: user.id }, input); revalidatePet(row.petId); } export async function createVaccinationForScope( scope: Scope, input: z.input, ) { const parsed = vaccinationInput.parse(input); const pet = await requirePet(scope, parsed.petId); const [row] = await db .insert(petVaccinations) .values({ householdId: scope.householdId, ...parsed, dueOn: parsed.dueOn ?? null, provider: parsed.provider ?? null, notes: parsed.notes ?? null, }) .returning(); if (!row) throw new Error("Vaccination was not created"); if (scope.userId) await syncPetDueDateReminder({ householdId: scope.householdId, entityType: "pets.vaccination", entityId: row.id, dueOn: row.dueOn, createdBy: scope.userId, title: `Vaccination due: ${pet.name}`, body: row.name, }); return row; } export async function updateVaccinationForScope( scope: Scope, input: { id: string } & z.input, ) { const parsed = idInput.and(vaccinationUpdateInput).parse(input); const row = await requireVaccination(scope, parsed.id); const pet = await requirePet(scope, row.petId); const { id, ...changes } = parsed; const [updated] = await db .update(petVaccinations) .set({ ...changes, updatedAt: new Date() }) .where(eq(petVaccinations.id, id)) .returning(); if (!updated) throw new Error("Vaccination not found"); if (scope.userId) await syncPetDueDateReminder({ householdId: scope.householdId, entityType: "pets.vaccination", entityId: id, dueOn: updated.dueOn, createdBy: scope.userId, title: `Vaccination due: ${pet.name}`, body: updated.name, }); return updated; } export async function deleteVaccinationForScope(scope: Scope, input: { id: string }) { const { id } = idInput.parse(input); await requireVaccination(scope, id); await cancelReminder("pets.vaccination", id); await db.delete(petVaccinations).where(eq(petVaccinations.id, id)); } export async function createVaccination(input: z.input) { const { household, user } = await getCurrentSession(); const row = await createVaccinationForScope( { householdId: household.id, userId: user.id }, input, ); revalidatePet(row.petId); return row; } export async function deleteVaccination(input: { id: string }) { const { household, user } = await getCurrentSession(); const row = await requireVaccination({ householdId: household.id, userId: user.id }, input.id); await deleteVaccinationForScope({ householdId: household.id, userId: user.id }, input); revalidatePet(row.petId); } export async function createPrescriptionForScope( scope: Scope, input: z.input, ) { const parsed = prescriptionInput.parse(input); const pet = await requirePet(scope, parsed.petId); const [row] = await db .insert(petPrescriptions) .values({ householdId: scope.householdId, ...parsed, instructions: parsed.instructions ?? null, prescribedOn: parsed.prescribedOn ?? null, expiresOn: parsed.expiresOn ?? null, refillsRemaining: parsed.refillsRemaining ?? null, }) .returning(); if (!row) throw new Error("Prescription was not created"); if (scope.userId) await syncPetDueDateReminder({ householdId: scope.householdId, entityType: "pets.prescription", entityId: row.id, dueOn: row.active ? row.expiresOn : null, createdBy: scope.userId, title: `Prescription due: ${pet.name}`, body: row.medication, }); return row; } export async function updatePrescriptionForScope( scope: Scope, input: { id: string } & z.input, ) { const parsed = idInput.and(prescriptionUpdateInput).parse(input); const row = await requirePrescription(scope, parsed.id); const pet = await requirePet(scope, row.petId); const { id, ...changes } = parsed; const [updated] = await db .update(petPrescriptions) .set({ ...changes, updatedAt: new Date() }) .where(eq(petPrescriptions.id, id)) .returning(); if (!updated) throw new Error("Prescription not found"); if (scope.userId) await syncPetDueDateReminder({ householdId: scope.householdId, entityType: "pets.prescription", entityId: id, dueOn: updated.active ? updated.expiresOn : null, createdBy: scope.userId, title: `Prescription due: ${pet.name}`, body: updated.medication, }); return updated; } export async function deletePrescriptionForScope(scope: Scope, input: { id: string }) { const { id } = idInput.parse(input); await requirePrescription(scope, id); await cancelReminder("pets.prescription", id); await db.delete(petPrescriptions).where(eq(petPrescriptions.id, id)); } export async function createPrescription(input: z.input) { const { household, user } = await getCurrentSession(); const row = await createPrescriptionForScope( { householdId: household.id, userId: user.id }, input, ); revalidatePet(row.petId); return row; } export async function deletePrescription(input: { id: string }) { const { household, user } = await getCurrentSession(); const row = await requirePrescription({ householdId: household.id, userId: user.id }, input.id); await deletePrescriptionForScope({ householdId: household.id, userId: user.id }, input); revalidatePet(row.petId); }