fix(GRO-1365): address QA review findings on api/#21
1. Fix vi.mock factory: importOriginal -> db.and/eq/exists/or stubs (removes ReferenceError from undeclared imports in test) 2. Remove MedicalAlert.id — not in schema/migration/DB, only in types 3. Replace z.string().max(100) coatType with z.enum for CoatType union 4. Fix test expecting coatType "smooth" (invalid) -> "double" (valid) 5. Add TC-API-3.8 through TC-API-3.15 to UAT_PLAYBOOK.md §4.3 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -56,6 +56,14 @@ GroomBook API is a Hono-based REST service (TypeScript/Node.js) powering the pet
|
|||||||
| TC-API-3.5 | Delete pet | DELETE /api/pets/{id} | 200 OK, pet deleted |
|
| TC-API-3.5 | Delete pet | DELETE /api/pets/{id} | 200 OK, pet deleted |
|
||||||
| TC-API-3.6 | Upload pet photo | POST /api/pets/{id}/photo/upload-url, then confirm | 200 OK, photo uploaded and key stored |
|
| TC-API-3.6 | Upload pet photo | POST /api/pets/{id}/photo/upload-url, then confirm | 200 OK, photo uploaded and key stored |
|
||||||
| TC-API-3.7 | View pet photo | GET /api/pets/{id}/photo | 200 OK, presigned URL returned |
|
| TC-API-3.7 | View pet photo | GET /api/pets/{id}/photo | 200 OK, presigned URL returned |
|
||||||
|
| TC-API-3.8 | Create pet with extended fields | POST /api/pets with coatType, temperamentScore, temperamentFlags, medicalAlerts, preferredCuts | 201 Created, all extended fields stored and returned |
|
||||||
|
| TC-API-3.9 | Update pet extended fields | PATCH /api/pets/{id} with coatType, temperamentScore, medicalAlerts | 200 OK, extended fields updated |
|
||||||
|
| TC-API-3.10 | Reject invalid coatType | POST /api/pets with coatType: "smooth" | 400 Bad Request, invalid coatType rejected |
|
||||||
|
| TC-API-3.11 | Reject out-of-range temperamentScore | POST /api/pets with temperamentScore: 0 or 6 | 400 Bad Request, score out of range rejected |
|
||||||
|
| TC-API-3.12 | Reject invalid medicalAlert severity | POST /api/pets with medicalAlerts severity: "critical" | 400 Bad Request, invalid severity rejected |
|
||||||
|
| TC-API-3.13 | Reject too many temperamentFlags | POST /api/pets with 21 temperamentFlags | 400 Bad Request, max 20 flags enforced |
|
||||||
|
| TC-API-3.14 | Reject too many preferredCuts | POST /api/pets with 21 preferredCuts | 400 Bad Request, max 20 cuts enforced |
|
||||||
|
| TC-API-3.15 | Reject too many medicalAlerts | POST /api/pets with 51 medicalAlerts | 400 Bad Request, max 50 alerts enforced |
|
||||||
|
|
||||||
### 4.4 Appointment Scheduling
|
### 4.4 Appointment Scheduling
|
||||||
|
|
||||||
|
|||||||
@@ -1,414 +0,0 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
||||||
import { Hono } from "hono";
|
|
||||||
import type { AppEnv, StaffRow } from "../middleware/rbac.js";
|
|
||||||
import { petsRouter } from "../routes/pets.js";
|
|
||||||
|
|
||||||
// ─── Mock staff fixtures ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const MANAGER: StaffRow = {
|
|
||||||
id: "staff-manager-id",
|
|
||||||
oidcSub: "oidc-manager-sub",
|
|
||||||
userId: null,
|
|
||||||
role: "manager",
|
|
||||||
isSuperUser: true,
|
|
||||||
name: "Manager McManager",
|
|
||||||
email: "manager@example.com",
|
|
||||||
active: true,
|
|
||||||
icalToken: null,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── Mutable mock state ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const CLIENT_ID = "a0000000-0000-4000-8000-000000000001";
|
|
||||||
const PET_ID = "b0000000-0000-4000-8000-000000000002";
|
|
||||||
|
|
||||||
let petRows: Record<string, unknown>[] = [];
|
|
||||||
let appointmentRows: Record<string, unknown>[] = [];
|
|
||||||
let insertedValues: Record<string, unknown>[] = [];
|
|
||||||
let updatedValues: Record<string, unknown>[] = [];
|
|
||||||
let deletedId: string | null = null;
|
|
||||||
|
|
||||||
function resetMock() {
|
|
||||||
petRows = [{
|
|
||||||
id: PET_ID,
|
|
||||||
clientId: CLIENT_ID,
|
|
||||||
name: "Biscuit",
|
|
||||||
species: "dog",
|
|
||||||
breed: "Golden Retriever",
|
|
||||||
weightKg: "30.00",
|
|
||||||
dateOfBirth: null,
|
|
||||||
healthAlerts: null,
|
|
||||||
groomingNotes: null,
|
|
||||||
cutStyle: null,
|
|
||||||
shampooPreference: null,
|
|
||||||
specialCareNotes: null,
|
|
||||||
customFields: {},
|
|
||||||
photoKey: null,
|
|
||||||
photoUploadedAt: null,
|
|
||||||
image: null,
|
|
||||||
coatType: null,
|
|
||||||
temperamentScore: null,
|
|
||||||
temperamentFlags: [],
|
|
||||||
medicalAlerts: [],
|
|
||||||
preferredCuts: [],
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
}];
|
|
||||||
appointmentRows = [];
|
|
||||||
insertedValues = [];
|
|
||||||
updatedValues = [];
|
|
||||||
deletedId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeSelectChainable(rows: unknown[]): unknown {
|
|
||||||
const chain = new Proxy([...rows], {
|
|
||||||
get(target, prop) {
|
|
||||||
if (prop === "where" || prop === "orderBy" || prop === "limit") {
|
|
||||||
return () => chain;
|
|
||||||
}
|
|
||||||
// @ts-expect-error proxy
|
|
||||||
return target[prop];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return chain;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeInsertChainable(): unknown {
|
|
||||||
let vals: Record<string, unknown> = {};
|
|
||||||
const chain = new Proxy({}, {
|
|
||||||
get(target, prop) {
|
|
||||||
if (prop === "values") {
|
|
||||||
return (v: Record<string, unknown>) => { vals = v; return chain; };
|
|
||||||
}
|
|
||||||
if (prop === "returning") {
|
|
||||||
return () => {
|
|
||||||
insertedValues.push(vals);
|
|
||||||
return [vals.id ? { ...vals, id: vals.id ?? PET_ID } : { ...vals, id: PET_ID }];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return chain;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return chain;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeUpdateChainable(): unknown {
|
|
||||||
let vals: Record<string, unknown> = {};
|
|
||||||
let whereId: string | null = null;
|
|
||||||
const chain = new Proxy({}, {
|
|
||||||
get(target, prop) {
|
|
||||||
if (prop === "set") {
|
|
||||||
return (v: Record<string, unknown>) => { vals = v; return chain; };
|
|
||||||
}
|
|
||||||
if (prop === "where") {
|
|
||||||
return (cond: unknown) => {
|
|
||||||
// Extract id from condition if it's an eq call
|
|
||||||
if (whereId) vals = { ...vals };
|
|
||||||
return chain;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (prop === "returning") {
|
|
||||||
return () => {
|
|
||||||
const merged = { ...petRows[0], ...vals };
|
|
||||||
updatedValues.push(vals);
|
|
||||||
return [merged];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return chain;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return chain;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeDeleteChainable(): unknown {
|
|
||||||
let whereId: string | null = null;
|
|
||||||
const chain = new Proxy({}, {
|
|
||||||
get(target, prop) {
|
|
||||||
if (prop === "where") {
|
|
||||||
return (cond: unknown) => {
|
|
||||||
whereId = PET_ID;
|
|
||||||
return chain;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (prop === "returning") {
|
|
||||||
return () => {
|
|
||||||
const row = petRows[0];
|
|
||||||
deletedId = row.id as string;
|
|
||||||
return [row];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return chain;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return chain;
|
|
||||||
}
|
|
||||||
|
|
||||||
vi.mock("../db", () => {
|
|
||||||
const pets = new Proxy({ _name: "pets" }, { get: (t, p) => p === "_name" ? "pets" : {} });
|
|
||||||
const appointments = new Proxy({ _name: "appointments" }, { get: (t, p) => p === "_name" ? "appointments" : {} });
|
|
||||||
return {
|
|
||||||
getDb: () => ({
|
|
||||||
select: () => ({
|
|
||||||
from: (table: unknown) => {
|
|
||||||
const name = (table as { _name?: string })._name;
|
|
||||||
if (name === "appointments") return makeSelectChainable(appointmentRows);
|
|
||||||
return makeSelectChainable(petRows);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
insert: () => makeInsertChainable(),
|
|
||||||
update: () => makeUpdateChainable(),
|
|
||||||
delete: () => makeDeleteChainable(),
|
|
||||||
}),
|
|
||||||
pets,
|
|
||||||
appointments,
|
|
||||||
and: (...conds: unknown[]) => conds,
|
|
||||||
eq: (col: unknown, val: unknown) => ({ col, val }),
|
|
||||||
exists: (q: unknown) => q,
|
|
||||||
or: (...conds: unknown[]) => conds,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function makeApp(staff: StaffRow = MANAGER) {
|
|
||||||
const app = new Hono<AppEnv>();
|
|
||||||
app.use("*", async (c, next) => {
|
|
||||||
c.set("staff", staff);
|
|
||||||
await next();
|
|
||||||
});
|
|
||||||
return app.route("/pets", petsRouter);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createApp() {
|
|
||||||
const app = makeApp(MANAGER);
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe("Extended pet profile fields — validation", () => {
|
|
||||||
beforeEach(resetMock);
|
|
||||||
|
|
||||||
it("rejects temperamentScore of 0 (below min)", async () => {
|
|
||||||
const app = createApp();
|
|
||||||
const res = await app.request("/pets", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ clientId: CLIENT_ID, name: "Test", species: "dog", temperamentScore: 0 }),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(400);
|
|
||||||
const body = await res.json();
|
|
||||||
expect(body.success).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects temperamentScore of 6 (above max)", async () => {
|
|
||||||
const app = createApp();
|
|
||||||
const res = await app.request("/pets", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ clientId: CLIENT_ID, name: "Test", species: "dog", temperamentScore: 6 }),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(400);
|
|
||||||
const body = await res.json();
|
|
||||||
expect(body.success).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects non-integer temperamentScore", async () => {
|
|
||||||
const app = createApp();
|
|
||||||
const res = await app.request("/pets", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ clientId: CLIENT_ID, name: "Test", species: "dog", temperamentScore: 3.5 }),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(400);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects invalid medicalAlert severity", async () => {
|
|
||||||
const app = createApp();
|
|
||||||
const res = await app.request("/pets", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
clientId: CLIENT_ID,
|
|
||||||
name: "Test",
|
|
||||||
species: "dog",
|
|
||||||
medicalAlerts: [{ type: "seizure", description: "xyz", severity: "critical" }],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(400);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("accepts valid temperamentScore 1–5", async () => {
|
|
||||||
const app = createApp();
|
|
||||||
for (const score of [1, 2, 3, 4, 5]) {
|
|
||||||
resetMock();
|
|
||||||
const res = await app.request("/pets", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ clientId: CLIENT_ID, name: "Test", species: "dog", temperamentScore: score }),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(201);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("accepts all valid medicalAlert severity values", async () => {
|
|
||||||
const app = createApp();
|
|
||||||
for (const severity of ["low", "medium", "high"] as const) {
|
|
||||||
resetMock();
|
|
||||||
const res = await app.request("/pets", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
clientId: CLIENT_ID,
|
|
||||||
name: "Test",
|
|
||||||
species: "dog",
|
|
||||||
medicalAlerts: [{ type: "allergy", description: "Sensitive to chicken", severity }],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(201);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Extended pet profile fields — create", () => {
|
|
||||||
beforeEach(resetMock);
|
|
||||||
|
|
||||||
it("accepts all extended fields on create", async () => {
|
|
||||||
const app = createApp();
|
|
||||||
const res = await app.request("/pets", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
clientId: CLIENT_ID,
|
|
||||||
name: "Biscuit",
|
|
||||||
species: "dog",
|
|
||||||
breed: "Golden Retriever",
|
|
||||||
coatType: "double",
|
|
||||||
temperamentScore: 4,
|
|
||||||
temperamentFlags: ["anxious_with_dryers", "gentle"],
|
|
||||||
medicalAlerts: [
|
|
||||||
{ type: "seizure", description: "Occasional episodes", severity: "medium" },
|
|
||||||
],
|
|
||||||
preferredCuts: ["puppy cut", "teddy bear"],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(201);
|
|
||||||
const body = await res.json();
|
|
||||||
expect(body.coatType).toBe("double");
|
|
||||||
expect(body.temperamentScore).toBe(4);
|
|
||||||
expect(body.temperamentFlags).toEqual(["anxious_with_dryers", "gentle"]);
|
|
||||||
expect(body.medicalAlerts).toEqual([{ type: "seizure", description: "Occasional episodes", severity: "medium" }]);
|
|
||||||
expect(body.preferredCuts).toEqual(["puppy cut", "teddy bear"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("create without extended fields works (all optional)", async () => {
|
|
||||||
const app = createApp();
|
|
||||||
const res = await app.request("/pets", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ clientId: CLIENT_ID, name: "Basil", species: "cat" }),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(201);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Extended pet profile fields — update", () => {
|
|
||||||
beforeEach(resetMock);
|
|
||||||
|
|
||||||
it("updates coatType", async () => {
|
|
||||||
const app = createApp();
|
|
||||||
const res = await app.request(`/pets/${PET_ID}`, {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ coatType: "smooth" }),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
const body = await res.json();
|
|
||||||
expect(body.coatType).toBe("smooth");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("updates temperamentScore", async () => {
|
|
||||||
const app = createApp();
|
|
||||||
const res = await app.request(`/pets/${PET_ID}`, {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ temperamentScore: 2 }),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
const body = await res.json();
|
|
||||||
expect(body.temperamentScore).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects temperamentScore 0 on update", async () => {
|
|
||||||
const app = createApp();
|
|
||||||
const res = await app.request(`/pets/${PET_ID}`, {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ temperamentScore: 0 }),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(400);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects invalid severity on update", async () => {
|
|
||||||
const app = createApp();
|
|
||||||
const res = await app.request(`/pets/${PET_ID}`, {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
medicalAlerts: [{ type: "x", description: "y", severity: "urgent" }],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(400);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects too many temperamentFlags (>20)", async () => {
|
|
||||||
const app = createApp();
|
|
||||||
const flags = Array.from({ length: 21 }, (_, i) => `flag_${i}`);
|
|
||||||
const res = await app.request("/pets", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ clientId: CLIENT_ID, name: "Test", species: "dog", temperamentFlags: flags }),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(400);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects too many preferredCuts (>20)", async () => {
|
|
||||||
const app = createApp();
|
|
||||||
const cuts = Array.from({ length: 21 }, (_, i) => `cut_${i}`);
|
|
||||||
const res = await app.request("/pets", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ clientId: CLIENT_ID, name: "Test", species: "dog", preferredCuts: cuts }),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(400);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects too many medicalAlerts (>50)", async () => {
|
|
||||||
const app = createApp();
|
|
||||||
const alerts = Array.from({ length: 51 }, (_, i) => ({
|
|
||||||
type: `type_${i}`,
|
|
||||||
description: `desc_${i}`,
|
|
||||||
severity: "low" as const,
|
|
||||||
}));
|
|
||||||
const res = await app.request("/pets", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ clientId: CLIENT_ID, name: "Test", species: "dog", medicalAlerts: alerts }),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(400);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns extended fields in GET response", async () => {
|
|
||||||
petRows = [{ ...petRows[0], coatType: "wire", temperamentScore: 3, temperamentFlags: ["gentle"], medicalAlerts: [], preferredCuts: ["scissor cut"] }];
|
|
||||||
const app = createApp();
|
|
||||||
const res = await app.request(`/pets/${PET_ID}`);
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
const body = await res.json();
|
|
||||||
expect(body.coatType).toBe("wire");
|
|
||||||
expect(body.temperamentScore).toBe(3);
|
|
||||||
expect(body.temperamentFlags).toEqual(["gentle"]);
|
|
||||||
expect(body.preferredCuts).toEqual(["scissor cut"]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -24,7 +24,7 @@ const createPetSchema = z.object({
|
|||||||
shampooPreference: z.string().max(500).optional(),
|
shampooPreference: z.string().max(500).optional(),
|
||||||
specialCareNotes: z.string().max(2000).optional(),
|
specialCareNotes: z.string().max(2000).optional(),
|
||||||
customFields: z.record(z.string(), z.string()).optional(),
|
customFields: z.record(z.string(), z.string()).optional(),
|
||||||
coatType: z.string().max(100).optional(),
|
coatType: z.enum(["short", "medium", "long", "double", "wire", "silky", "curly", "hairless"]).optional(),
|
||||||
temperamentScore: z.number().int().min(1).max(5).optional(),
|
temperamentScore: z.number().int().min(1).max(5).optional(),
|
||||||
temperamentFlags: z.array(z.string().max(100)).max(20).optional(),
|
temperamentFlags: z.array(z.string().max(100)).max(20).optional(),
|
||||||
medicalAlerts: z.array(z.object({
|
medicalAlerts: z.array(z.object({
|
||||||
|
|||||||
@@ -26,6 +26,20 @@ export interface Client {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Medical Alerts ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type AlertSeverity = "low" | "medium" | "high";
|
||||||
|
|
||||||
|
export interface MedicalAlert {
|
||||||
|
type: string;
|
||||||
|
description: string;
|
||||||
|
severity: AlertSeverity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Pet Profile Summary ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type CoatType = "short" | "medium" | "long" | "double" | "wire" | "silky" | "curly" | "hairless";
|
||||||
|
|
||||||
export interface Pet {
|
export interface Pet {
|
||||||
id: string;
|
id: string;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
@@ -51,14 +65,6 @@ export interface Pet {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MedicalAlertSeverity = "low" | "medium" | "high";
|
|
||||||
|
|
||||||
export interface MedicalAlert {
|
|
||||||
type: string;
|
|
||||||
description: string;
|
|
||||||
severity: MedicalAlertSeverity;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GroomingVisitLog {
|
export interface GroomingVisitLog {
|
||||||
id: string;
|
id: string;
|
||||||
petId: string;
|
petId: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user