diff --git a/apps/api/src/__tests__/petProfileSummary.test.ts b/apps/api/src/__tests__/petProfileSummary.test.ts index 66c1e6f..38b138c 100644 --- a/apps/api/src/__tests__/petProfileSummary.test.ts +++ b/apps/api/src/__tests__/petProfileSummary.test.ts @@ -220,7 +220,9 @@ vi.mock("../db/index.js", () => { desc: vi.fn((c: unknown) => c), eq: vi.fn((_col: unknown, _val: unknown) => ({ col: _col, val: _val })), exists: vi.fn(() => true), + gte: vi.fn((a: unknown, b: unknown) => ({ col: a, val: b })), or: vi.fn((a: unknown, b: unknown) => [a, b]), + sql: vi.fn((str: string) => str), }; }); @@ -292,6 +294,54 @@ describe("GET /:id/profile-summary", () => { }); }); +describe("GET /:id/profile-summary — visitCount", () => { + beforeEach(resetMock); + + it("returns visitCount >= 2 when pet has 2+ completed appointments", async () => { + const app = makeApp(MANAGER); + // Add a second completed appointment + mock.appointments = [ + ...mock.appointments, + { + id: "appt-completed-2", + clientId: CLIENT_ID, + petId: PET_ID, + serviceId: "service-1", + staffId: "staff-groomer-id", + batherStaffId: null, + status: "completed", + startTime: new Date("2024-07-01T09:00:00Z"), + endTime: new Date("2024-07-01T11:00:00Z"), + notes: null, + priceCents: 6000, + seriesId: null, + seriesIndex: null, + groupId: null, + confirmationStatus: "confirmed", + confirmedAt: null, + cancelledAt: null, + confirmationToken: null, + customerNotes: null, + createdAt: new Date("2024-06-15"), + updatedAt: new Date("2024-06-15"), + }, + ]; + const res = await app.request(`/pets/${PET_ID}/profile-summary`); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.visitCount).toBeGreaterThanOrEqual(2); + }); + + it("returns visitCount = 0 when no completed appointments", async () => { + const app = makeApp(MANAGER); + mock.appointments = mock.appointments.map((a) => ({ ...a, status: "cancelled" })); + const res = await app.request(`/pets/${PET_ID}/profile-summary`); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.visitCount).toBe(0); + }); +}); + describe("GET /:id/profile-summary — empty history", () => { beforeEach(resetMock); diff --git a/apps/api/src/routes/pets.ts b/apps/api/src/routes/pets.ts index 52a4b34..f8b6440 100644 --- a/apps/api/src/routes/pets.ts +++ b/apps/api/src/routes/pets.ts @@ -1,7 +1,7 @@ import { Hono } from "hono"; import { zValidator } from "@hono/zod-validator"; import { z } from "zod/v3"; -import { and, desc, eq, exists, getDb, groomingVisitLogs, or, pets, appointments, staff, services } from "../db/index.js"; +import { and, desc, eq, exists, getDb, gte, groomingVisitLogs, or, pets, appointments, staff, services, sql } from "../db/index.js"; import type { AppEnv } from "../middleware/rbac.js"; import { getPresignedUploadUrl, @@ -362,13 +362,10 @@ petsRouter.get("/:id/profile-summary", async (c) => { const lastVisitDate = historyRows[0]?.groomedAt?.toISOString() ?? null; // Completed appointment count for this pet - const countResult = await db - .select({ count: appointments.id }) + const [{ count: visitCount }] = await db + .select({ count: sql`count(*)::int` }) .from(appointments) - .where(and(eq(appointments.petId, petId), eq(appointments.status, "completed"))) - .limit(1); - - const visitCount = countResult.length; + .where(and(eq(appointments.petId, petId), eq(appointments.status, "completed"))); // Upcoming appointment: next scheduled or confirmed const [nextAppt] = await db @@ -388,7 +385,8 @@ petsRouter.get("/:id/profile-summary", async (c) => { .where( and( eq(appointments.petId, petId), - or(eq(appointments.status, "scheduled"), eq(appointments.status, "confirmed")) + or(eq(appointments.status, "scheduled"), eq(appointments.status, "confirmed")), + gte(appointments.startTime, new Date()) ) ) .orderBy(appointments.startTime)