Compare commits

..

16 Commits

Author SHA1 Message Date
Flea Flicker 47c29ecbc2 Promote to UAT: GRO-2105 BookingFlow/RescheduleFlow availability fix (#47)
CI / Test (push) Successful in 17s
CI / Lint & Typecheck (push) Successful in 23s
CI / Build & Push Docker Image (push) Successful in 19s
CI / Test (pull_request) Failing after 10m34s
CI / Lint & Typecheck (pull_request) Failing after 10m34s
CI / Build & Push Docker Image (pull_request) Has been skipped
2026-06-02 19:17:03 +00:00
The Dogfather de7386e47a Promote to UAT: GRO-2094 React bootstrap error instrumentation (#45)
CI / Test (push) Successful in 23s
CI / Lint & Typecheck (push) Successful in 30s
CI / Build & Push Docker Image (push) Successful in 13s
Co-authored-by: The Dogfather <20+gb_dogfather@noreply.git.farh.net>
Co-committed-by: The Dogfather <20+gb_dogfather@noreply.git.farh.net>
2026-06-02 18:42:25 +00:00
The Dogfather ec29f71974 Merge pull request 'Promote to UAT: GRO-2012 RescheduleFlow portalSessionId fallback' (#39) from dev into uat
CI / Test (push) Successful in 21s
CI / Lint & Typecheck (push) Successful in 30s
CI / Build & Push Docker Image (push) Successful in 10s
CI / Test (pull_request) Successful in 21s
CI / Lint & Typecheck (pull_request) Successful in 28s
CI / Build & Push Docker Image (pull_request) Successful in 13s
2026-06-01 17:46:35 +00:00
The Dogfather bd2a0d9516 Merge pull request 'Promote dev -> uat: GRO-2011 login-blank fix (+ GRO-1867)' (#37) from dev into uat
CI / Test (push) Successful in 19s
CI / Lint & Typecheck (push) Successful in 23s
CI / Build & Push Docker Image (push) Successful in 10s
2026-06-01 16:38:14 +00:00
The Dogfather 0e5e9d1f16 Merge pull request 'chore: promote dev → uat (GRO-1829 SW fix)' (#32) from dev into uat
CI / Test (push) Successful in 13s
CI / Lint & Typecheck (push) Successful in 23s
CI / Build & Push Docker Image (push) Successful in 15s
Merge: promote dev → uat (GRO-1829 SW fix)
2026-05-27 02:27:32 +00:00
The Dogfather 3b4d0f15f6 Merge pull request 'chore: promote dev → uat (GRO-1795 StatusBadge)' (#28) from dev into uat
CI / Lint & Typecheck (push) Successful in 17s
CI / Test (push) Successful in 13s
CI / Build & Push Docker Image (push) Successful in 34s
Merge PR #28: promote dev → uat (GRO-1795 StatusBadge)
2026-05-26 13:23:52 +00:00
The Dogfather 87939e5413 Merge pull request 'chore: promote dev → uat (GRO-1794 booking analytics)' (#27) from dev into uat
CI / Test (push) Successful in 19s
CI / Lint & Typecheck (push) Successful in 22s
CI / Build & Push Docker Image (push) Successful in 12s
Merge dev → uat: GRO-1794 booking funnel analytics events
2026-05-26 13:16:39 +00:00
The Dogfather 4e3a038bf3 Merge pull request 'Promote dev → uat (GRO-1793: dynamic time slots)' (#25) from dev into uat
CI / Test (push) Successful in 14s
CI / Lint & Typecheck (push) Successful in 16s
CI / Build & Push Docker Image (push) Failing after 6s
Promote dev → uat: GRO-1793 dynamic portal time slots (#25)
2026-05-26 13:02:16 +00:00
Lint Roller 8349ea00de Merge pull request 'promote: dev → uat (GRO-1757 SSO auto-provision fix)' (#19) from dev into uat
CI / Test (push) Successful in 13s
CI / Lint & Typecheck (push) Successful in 33s
CI / Build & Push Docker Image (push) Successful in 14s
CI / Test (pull_request) Successful in 19s
CI / Lint & Typecheck (pull_request) Successful in 24s
CI / Build & Push Docker Image (pull_request) Successful in 15s
promote: dev → uat (GRO-1757 SSO auto-provision fix)
2026-05-25 23:48:10 +00:00
The Dogfather 0306c7fbd9 Merge pull request 'chore(GRO-1592): promote dev→uat SSO session cookie fix' (#16) from promote-uat-gro1592 into uat
CI / Test (push) Successful in 12s
CI / Lint & Typecheck (push) Successful in 18s
CI / Build & Push Docker Image (push) Failing after 39s
2026-05-23 14:13:43 +00:00
Chris Farhood 93da2f1dd8 chore: promote dev→uat for GRO-1592 SSO session cookie fix
CI / Lint & Typecheck (pull_request) Successful in 17s
CI / Test (pull_request) Successful in 18s
CI / Build & Push Docker Image (pull_request) Failing after 41s
- Fixed frontend auth client baseURL fallback to use window.location.origin
- Added UAT test coverage (TC-AUTH-5.3.4)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-23 14:13:12 +00:00
The Dogfather 62cbfe4e43 Merge pull request 'promote: dev → uat (GRO-1173 buffer rules + GRO-1470 pet save persistence)' (#14) from dev into uat
CI / Test (push) Successful in 14s
CI / Lint & Typecheck (push) Successful in 19s
CI / Build & Push Docker Image (push) Successful in 9s
promote: dev → uat (GRO-1173 buffer rules + GRO-1470 pet save persistence) (#14)

Merged-By: The Dogfather (CTO)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-21 19:46:41 +00:00
The Dogfather db6a2a1bbf Merge pull request 'promote: dev → uat (Renovate config, GRO-1081)' (#11) from dev into uat
promote: dev → uat (Renovate config, GRO-1081)

Merge PR #11: dev → uat promotion
Includes: chore: add Renovate config (GRO-1081)
2026-05-20 12:42:04 +00:00
The Dogfather 032a3796ba Merge pull request 'chore: promote dev to uat (CI Docker registry fix)' (#10) from dev into uat
chore: promote dev to uat (CI Docker registry fix) (#10)

Promotes GRO-1348 CI registry fix to UAT.
2026-05-20 11:17:21 +00:00
the-dogfather-cto[bot] cac8fc947e chore(GRO-1289): promote dev to uat — add UAT_PLAYBOOK.md
chore(GRO-1289): promote dev to uat — add UAT_PLAYBOOK.md
2026-05-14 21:13:56 +00:00
the-dogfather-cto[bot] 592be1301c chore: promote dev to uat (#3)
chore: promote dev to uat
2026-05-11 13:19:33 +00:00
3 changed files with 118 additions and 25 deletions
+13 -5
View File
@@ -186,18 +186,26 @@ export const { signIn, signOut, useSession, changePassword } = authClient;
| TC-WEB-5.12.3 | Confirm appointment | Click confirm on pending appointment | Appointment status updated to confirmed |
| TC-WEB-5.12.4 | Cancel appointment | Click cancel on appointment | Appointment marked as cancelled |
#### 5.12b Dynamic Portal Time Slots (GRO-1793)
#### 5.12b Dynamic Portal Time Slots (GRO-1793, GRO-2105)
| # | Scenario | Steps | Expected |
|---|----------|-------|----------|
| TC-WEB-5.12.5 | BookingFlow dynamic slots | Open Book New, select pet and service, pick a date | Time slots fetched from API; "Checking availability…" shown while loading |
| TC-WEB-5.12.5 | BookingFlow dynamic slots | Open Book New, select pet and service, pick a date | `GET /api/book/availability?serviceId=<selected>&date=<picked>`; "Checking availability…" shown while loading; slot list rendered |
| TC-WEB-5.12.6 | BookingFlow slots match wizard | Compare BookingFlow slot times with public booking wizard for same date | Same slots displayed |
| TC-WEB-5.12.7 | BookingFlow error state | Mock API failure on availability fetch | "Failed to load time slots" error shown |
| TC-WEB-5.12.7 | BookingFlow error state | Mock API failure on availability fetch (4xx/5xx OR a 200 with non-array body) | "Failed to load time slots" error shown and the page stays interactive (no white screen) |
| TC-WEB-5.12.8 | BookingFlow no slots | Select date with no availability | "No available slots on this date" shown |
| TC-WEB-5.12.9 | RescheduleFlow dynamic slots | Open reschedule, pick a new date | Time slots fetched from API; loading state shown |
| TC-WEB-5.12.10 | RescheduleFlow error state | Mock API failure on availability fetch | "Failed to load time slots" error shown |
| TC-WEB-5.12.9 | RescheduleFlow dynamic slots | Open reschedule, pick a new date | `GET /api/book/availability?serviceId=<appt.serviceId>&date=<picked>`; loading state shown; slot list rendered |
| TC-WEB-5.12.10 | RescheduleFlow error state | Mock API failure on availability fetch (4xx/5xx OR a 200 with non-array body) | "Failed to load time slots" error shown and the page stays interactive (no white screen) |
| TC-WEB-5.12.11 | RescheduleFlow no slots | Select date with no availability | "No available slots on this date" shown |
> **GRO-2105 regression note:** prior to the fix, both `BookingFlow` and
> `RescheduleFlow` called `/api/book/availability` with only `date=…`, so the
> API responded 400 `{error:"serviceId and date are required"}`. The React
> handler then `.map()`'d that error object, throwing `TypeError: ee.map is
> not a function` and wiping `<div id="root">`. The fix ensures both flows
> include `serviceId` in the query string and surface the API's error string
> (or "Failed to load time slots") instead of crashing.
#### 5.12c Waitlist/Booking Status Badges (GRO-1795)
| # | Scenario | Steps | Expected |
+37 -2
View File
@@ -530,7 +530,7 @@ describe("RescheduleFlow dynamic time slots", () => {
});
});
it("calls /api/book/availability with the selected date", async () => {
it("calls /api/book/availability with the serviceId and selected date", async () => {
vi.mocked(global.fetch).mockResolvedValue({
ok: true,
json: async () => ["9:00 AM"] as string[],
@@ -544,7 +544,7 @@ describe("RescheduleFlow dynamic time slots", () => {
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith(
"/api/book/availability?date=2027-02-20",
"/api/book/availability?serviceId=service-1&date=2027-02-20",
expect.objectContaining({
headers: expect.objectContaining({ "X-Impersonation-Session-Id": "test-session-id" }),
})
@@ -552,6 +552,41 @@ describe("RescheduleFlow dynamic time slots", () => {
});
});
it("shows error message when API returns a 4xx error object instead of an array", async () => {
vi.mocked(global.fetch).mockResolvedValue({
ok: false,
status: 400,
json: async () => ({ error: "serviceId and date are required" }),
} as Response);
const { RescheduleFlow } = await import("../portal/sections/Appointments.tsx");
render(<RescheduleFlow appointment={RESCHEDULE_APPT} onClose={() => {}} sessionId="test-session-id" />);
const dateInput = screen.getByLabelText(/date/i) || screen.getByRole("textbox", { name: /date/i });
fireEvent.change(dateInput, { target: { value: "2027-02-20" } });
await waitFor(() => {
expect(screen.getByText(/serviceId and date are required/i)).toBeInTheDocument();
});
});
it("shows generic error when API returns 200 but body is not an array", async () => {
vi.mocked(global.fetch).mockResolvedValue({
ok: true,
json: async () => ({ error: "serviceId and date are required" }),
} as Response);
const { RescheduleFlow } = await import("../portal/sections/Appointments.tsx");
render(<RescheduleFlow appointment={RESCHEDULE_APPT} onClose={() => {}} sessionId="test-session-id" />);
const dateInput = screen.getByLabelText(/date/i) || screen.getByRole("textbox", { name: /date/i });
fireEvent.change(dateInput, { target: { value: "2027-02-20" } });
await waitFor(() => {
expect(screen.getByText(/Failed to load time slots/i)).toBeInTheDocument();
});
});
it("re-fetches slots when date changes", async () => {
vi.mocked(global.fetch)
.mockResolvedValueOnce({
+68 -18
View File
@@ -2,6 +2,35 @@ import React, { useState, useEffect } from 'react';
import { Calendar, Clock, Plus, ChevronRight, ChevronDown, Loader2 } from 'lucide-react';
import { ANALYTICS_EVENTS, fireAnalyticsEvent } from '../../lib/analytics';
// ─── Availability fetch helper ───────────────────────────────────────────────
// Returns ISO startTime strings for the given service/date, or an error message.
// Validates HTTP status and that the body is actually an array — the API
// responds with `{error: "..."}` on 4xx, and we must not treat that as slots.
const AVAILABILITY_ERROR_MESSAGE = 'Failed to load time slots';
async function fetchAvailability(
params: { serviceId: string; date: string },
sessionId: string | null,
): Promise<{ times: string[]; error: string | null }> {
const url = `/api/book/availability?${new URLSearchParams(params).toString()}`;
const headers: Record<string, string> = {};
if (sessionId) headers['X-Impersonation-Session-Id'] = sessionId;
try {
const res = await fetch(url, { headers });
if (!res.ok) {
const body = (await res.json().catch(() => ({}))) as { error?: string };
return { times: [], error: body.error ?? `${AVAILABILITY_ERROR_MESSAGE} (HTTP ${res.status})` };
}
const data: unknown = await res.json();
if (!Array.isArray(data)) {
return { times: [], error: AVAILABILITY_ERROR_MESSAGE };
}
return { times: data as string[], error: null };
} catch {
return { times: [], error: AVAILABILITY_ERROR_MESSAGE };
}
}
export interface Appointment {
id: string;
petId: string;
@@ -595,19 +624,29 @@ export function RescheduleFlow({
useEffect(() => {
if (!selectedDate || !sessionId) {
setAvailableTimes([]);
setSlotsError(null);
return;
}
const params = new URLSearchParams({ date: selectedDate });
if (!appt.serviceId) {
setAvailableTimes([]);
setSlotsError('Failed to load time slots');
return;
}
let cancelled = false;
setSlotsLoading(true);
setSlotsError(null);
fetch(`/api/book/availability?${params.toString()}`, {
headers: { "X-Impersonation-Session-Id": sessionId ?? "" },
})
.then((r) => r.json() as Promise<string[]>)
.then(setAvailableTimes)
.catch(() => setSlotsError('Failed to load time slots'))
.finally(() => setSlotsLoading(false));
}, [selectedDate, sessionId]);
fetchAvailability({ serviceId: appt.serviceId, date: selectedDate }, sessionId).then(
({ times, error }) => {
if (cancelled) return;
setAvailableTimes(times);
setSlotsError(error);
setSlotsLoading(false);
},
);
return () => {
cancelled = true;
};
}, [selectedDate, sessionId, appt.serviceId]);
async function handleSubmit() {
if (!selectedDate || !selectedTime) return;
@@ -766,19 +805,30 @@ function BookingFlow({ onClose, sessionId }: BookingFlowProps) {
useEffect(() => {
if (!selectedDate || !sessionId) {
setAvailableTimes([]);
setSlotsError(null);
return;
}
const params = new URLSearchParams({ date: selectedDate });
const serviceId = selectedServices[0]?.id;
if (!serviceId) {
setAvailableTimes([]);
setSlotsError('Failed to load time slots');
return;
}
let cancelled = false;
setSlotsLoading(true);
setSlotsError(null);
fetch(`/api/book/availability?${params.toString()}`, {
headers: { "X-Impersonation-Session-Id": sessionId ?? "" },
})
.then((r) => r.json() as Promise<string[]>)
.then(setAvailableTimes)
.catch(() => setSlotsError('Failed to load time slots'))
.finally(() => setSlotsLoading(false));
}, [selectedDate, sessionId]);
fetchAvailability({ serviceId, date: selectedDate }, sessionId).then(
({ times, error }) => {
if (cancelled) return;
setAvailableTimes(times);
setSlotsError(error);
setSlotsLoading(false);
},
);
return () => {
cancelled = true;
};
}, [selectedDate, sessionId, selectedServices]);
useEffect(() => {
const fetchData = async () => {