Promote uat → main (PROD): GRO-2319 portal waitlist surfacing + seed (#207)
CI / Test (push) Successful in 23s
CI / Lint & Typecheck (push) Successful in 26s
CI / Build & Push Docker Images (push) Successful in 41s

Co-authored-by: Flea Flicker <22+gb_flea@noreply.git.farh.net>
Co-committed-by: Flea Flicker <22+gb_flea@noreply.git.farh.net>
This commit was merged in pull request #207.
This commit is contained in:
2026-06-10 08:58:26 +00:00
committed by Scrubs McBarkley
parent 31404befee
commit 47e2021cf4
4 changed files with 165 additions and 9 deletions
+73
View File
@@ -39,11 +39,17 @@ const APPOINTMENT = {
let selectSessionRow: Record<string, unknown> | null = null;
let selectAppointmentRow: Record<string, unknown> | null = null;
let selectWaitlistRows: Record<string, unknown>[] = [];
let selectPetRows: Record<string, unknown>[] = [];
let selectStaffRows: Record<string, unknown>[] = [];
let updatedValues: Record<string, unknown>[] = [];
function resetMock() {
selectSessionRow = null;
selectAppointmentRow = null;
selectWaitlistRows = [];
selectPetRows = [];
selectStaffRows = [];
updatedValues = [];
}
@@ -72,6 +78,12 @@ vi.mock("@groombook/db", () => {
{ get: (t, p) => (p === "_name" ? "appointments" : { table: "appointments", column: p }) }
);
const mkTable = (name: string) =>
new Proxy({ _name: name }, { get: (t, p) => (p === "_name" ? name : { table: name, column: p }) });
const waitlistEntries = mkTable("waitlistEntries");
const pets = mkTable("pets");
const staff = mkTable("staff");
return {
getDb: () => ({
select: () => ({
@@ -82,6 +94,15 @@ vi.mock("@groombook/db", () => {
if (table._name === "appointments") {
return makeChainable(selectAppointmentRow ? [selectAppointmentRow] : []);
}
if (table._name === "waitlistEntries") {
return makeChainable(selectWaitlistRows);
}
if (table._name === "pets") {
return makeChainable(selectPetRows);
}
if (table._name === "staff") {
return makeChainable(selectStaffRows);
}
return makeChainable([]);
},
}),
@@ -102,8 +123,12 @@ vi.mock("@groombook/db", () => {
}),
impersonationSessions,
appointments,
waitlistEntries,
pets,
staff,
eq: vi.fn(),
and: vi.fn(),
inArray: vi.fn(),
};
});
@@ -125,6 +150,54 @@ function jsonPatch(path: string, body: unknown, headers?: Record<string, string>
beforeEach(() => resetMock());
// GRO-2319 item 2: the portal Upcoming list renders active waitlist entries as
// synthetic `waitlisted` cards, so GET /portal/appointments must surface them.
describe("GET /portal/appointments (waitlist surfacing — GRO-2319)", () => {
it("returns active waitlist entries as synthetic waitlisted cards", async () => {
selectSessionRow = ACTIVE_SESSION;
selectAppointmentRow = { ...APPOINTMENT };
selectWaitlistRows = [
{
id: "11111111-1111-1111-1111-111111111111",
petId: "pet-1",
serviceId: "svc-1",
preferredDate: "2099-01-01",
preferredTime: "13:00:00",
},
];
selectPetRows = [{ id: "pet-1", name: "Rex", photoKey: null }];
const res = await app.request("/portal/appointments", {
headers: { "X-Impersonation-Session-Id": SESSION_ID },
});
expect(res.status).toBe(200);
const body = await res.json();
const waitlistCard = body.appointments.find(
(a: { status: string }) => a.status === "waitlisted",
);
expect(waitlistCard).toBeTruthy();
expect(waitlistCard.id).toBe("waitlist:11111111-1111-1111-1111-111111111111");
expect(waitlistCard.pet.name).toBe("Rex");
expect(waitlistCard.confirmationStatus).toBeNull();
// startTime is derived from preferredDate + preferredTime so the card sorts
// and classifies as Upcoming.
expect(waitlistCard.startTime).toBeTruthy();
});
it("omits the waitlist section when the client has no active entries", async () => {
selectSessionRow = ACTIVE_SESSION;
selectAppointmentRow = { ...APPOINTMENT };
selectWaitlistRows = [];
const res = await app.request("/portal/appointments", {
headers: { "X-Impersonation-Session-Id": SESSION_ID },
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.appointments.some((a: { status: string }) => a.status === "waitlisted")).toBe(false);
});
});
describe("PATCH /portal/appointments/:id/notes", () => {
it("returns updated appointment with safe fields only", async () => {
selectSessionRow = ACTIVE_SESSION;