fix(GRO-1470): add portal PATCH /pets/:id persistence + expand GET /pets response
CI / Lint & Typecheck (pull_request) Successful in 16s
CI / Test (pull_request) Successful in 20s
CI / Build & Push Docker Image (pull_request) Successful in 30s

- Add PATCH /api/portal/pets/:id endpoint for client-facing pet profile saves
  (handlePetSave was calling this but the route didn't exist)
- Expand GET /api/portal/pets to return full pet fields including coatType,
  petSizeCategory, cutStyle, shampooPreference, specialCareNotes, customFields
- Add portalPetUpdateSchema Zod validator matching the staff-facing pets schema
- Add 404 guard for not-found updates, 403 for client-pet mismatch
- Return full updated pet record in PATCH response

Refs: GRO-1178, GRO-1470

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-05-21 19:28:47 +00:00
parent 48d0e687b9
commit 7d27fb85c6
+81 -1
View File
@@ -149,9 +149,89 @@ portalRouter.get("/pets", async (c) => {
const clientId = c.get("portalClientId");
const clientPets = await db.select().from(pets).where(eq(pets.clientId, clientId));
return c.json(clientPets.map(p => ({ id: p.id, name: p.name, breed: p.breed, weight: p.weightKg, birthDate: p.dateOfBirth, photoUrl: p.photoKey, notes: p.groomingNotes })));
return c.json(clientPets.map(p => ({
id: p.id,
name: p.name,
species: p.species,
breed: p.breed,
weightKg: p.weightKg ? Number(p.weightKg) : null,
dateOfBirth: p.dateOfBirth ? new Date(p.dateOfBirth).toISOString() : null,
healthAlerts: p.healthAlerts,
groomingNotes: p.groomingNotes,
cutStyle: p.cutStyle,
shampooPreference: p.shampooPreference,
specialCareNotes: p.specialCareNotes,
coatType: p.coatType,
petSizeCategory: p.petSizeCategory,
photoKey: p.photoKey,
photoUploadedAt: p.photoUploadedAt ? new Date(p.photoUploadedAt).toISOString() : null,
customFields: p.customFields,
})));
});
const portalPetUpdateSchema = z.object({
name: z.string().min(1).max(200).optional(),
species: z.string().min(1).max(100).optional(),
breed: z.string().max(200).optional().nullable(),
weightKg: z.number().positive().optional().nullable(),
dateOfBirth: z.string().datetime().optional().nullable(),
healthAlerts: z.string().max(2000).optional().nullable(),
groomingNotes: z.string().max(2000).optional().nullable(),
cutStyle: z.string().max(500).optional().nullable(),
shampooPreference: z.string().max(500).optional().nullable(),
specialCareNotes: z.string().max(2000).optional().nullable(),
coatType: z.enum(["smooth", "double", "wire", "curly", "long", "hairless"]).optional().nullable(),
petSizeCategory: z.enum(["small", "medium", "large", "xlarge"]).optional().nullable(),
customFields: z.record(z.string(), z.string()).optional(),
});
portalRouter.patch("/pets/:id",
zValidator("json", portalPetUpdateSchema),
async (c) => {
const db = getDb();
const petId = c.req.param("id");
const clientId = c.get("portalClientId");
const body = c.req.valid("json");
const [existing] = await db.select().from(pets).where(eq(pets.id, petId)).limit(1);
if (!existing) return c.json({ error: "Not found" }, 404);
if (existing.clientId !== clientId) return c.json({ error: "Forbidden" }, 403);
const { weightKg, dateOfBirth, ...rest } = body;
const [updated] = await db
.update(pets)
.set({
...rest,
weightKg: weightKg != null ? String(weightKg) : undefined,
dateOfBirth: dateOfBirth != null ? new Date(dateOfBirth) : undefined,
updatedAt: new Date(),
})
.where(eq(pets.id, petId))
.returning();
if (!updated) return c.json({ error: "Not found" }, 404);
return c.json({
id: updated.id,
name: updated.name,
species: updated.species,
breed: updated.breed,
weightKg: updated.weightKg ? Number(updated.weightKg) : null,
dateOfBirth: updated.dateOfBirth ? new Date(updated.dateOfBirth).toISOString() : null,
healthAlerts: updated.healthAlerts,
groomingNotes: updated.groomingNotes,
cutStyle: updated.cutStyle,
shampooPreference: updated.shampooPreference,
specialCareNotes: updated.specialCareNotes,
coatType: updated.coatType,
petSizeCategory: updated.petSizeCategory,
photoKey: updated.photoKey,
photoUploadedAt: updated.photoUploadedAt ? new Date(updated.photoUploadedAt).toISOString() : null,
customFields: updated.customFields,
});
}
);
portalRouter.get("/invoices", async (c) => {
const db = getDb();
const clientId = c.get("portalClientId");