feat(pets): add scoped records and reminders
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
"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<ApiAuthContext, "householdId" | "userId">;
|
||||
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<typeof petInput>) {
|
||||
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<typeof petInput>) {
|
||||
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<typeof petUpdateInput>,
|
||||
) {
|
||||
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<typeof petUpdateInput>) {
|
||||
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<typeof appointmentInput>,
|
||||
) {
|
||||
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<typeof appointmentUpdateInput>,
|
||||
) {
|
||||
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 createVaccinationForScope(
|
||||
scope: Scope,
|
||||
input: z.input<typeof vaccinationInput>,
|
||||
) {
|
||||
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<typeof vaccinationUpdateInput>,
|
||||
) {
|
||||
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 createPrescriptionForScope(
|
||||
scope: Scope,
|
||||
input: z.input<typeof prescriptionInput>,
|
||||
) {
|
||||
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<typeof prescriptionUpdateInput>,
|
||||
) {
|
||||
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));
|
||||
}
|
||||
Reference in New Issue
Block a user