From df053ce0f4069eb1dcdb7d8442e23fc8329cec7a Mon Sep 17 00:00:00 2001 From: Shreeya Adhikari Date: Wed, 1 Jul 2026 15:17:43 -0400 Subject: [PATCH 1/4] Confirm type definitions in types.ts + Define machine input type --- apps/app-portal/src/lib/status/types.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/app-portal/src/lib/status/types.ts b/apps/app-portal/src/lib/status/types.ts index 97a1a644..7efd077e 100644 --- a/apps/app-portal/src/lib/status/types.ts +++ b/apps/app-portal/src/lib/status/types.ts @@ -9,8 +9,17 @@ export type DashboardBranch = export type ApplicantStatus = { userId: string; applicationStatus: ApplicationStatus; - decisionStatus: DecisionStatus; + decisionStatus?: DecisionStatus; rsvpStatus: RsvpStatus; +} +export type MachineInput = { + user: ApplicantStatus; + dates: { + registrationOpen: Date; + confirmBy: Date; + }; + showDecision: boolean; + now: Date; }; export type ApplicationStatus = "in-progress" | "submitted"; From baf51d6d3aa360ed8478bbadbf21d69633fd95cb Mon Sep 17 00:00:00 2001 From: Shreeya Adhikari Date: Wed, 1 Jul 2026 15:21:46 -0400 Subject: [PATCH 2/4] fix status machine branch logic --- apps/app-portal/src/lib/status/machine.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/app-portal/src/lib/status/machine.ts b/apps/app-portal/src/lib/status/machine.ts index 77dae8c0..81d4474f 100644 --- a/apps/app-portal/src/lib/status/machine.ts +++ b/apps/app-portal/src/lib/status/machine.ts @@ -11,10 +11,12 @@ export function returnDashboardBranch( return "pre-registration"; } - if (!showDecision) { - return user.applicationStatus === "in-progress" - ? "in-progress" - : "submitted"; + if (user.applicationStatus !== "submitted") { + return "in-progress"; + } + + if (!showDecision || !user.decisionStatus || user.decisionStatus === "pending") { + return "submitted"; } switch (user.decisionStatus) { @@ -25,8 +27,6 @@ export function returnDashboardBranch( case "declined": return "declined"; default: - return user.applicationStatus === "in-progress" - ? "in-progress" - : "submitted"; + return "submitted"; } -} +} \ No newline at end of file From b0cae0a98bc7c5154c93126febc93a2ec501ed3c Mon Sep 17 00:00:00 2001 From: Shreeya Adhikari Date: Wed, 1 Jul 2026 15:29:22 -0400 Subject: [PATCH 3/4] refactor machine to use MachineInput+ update route and fetchPortalStatus --- .../app-portal/src/app/api/v1/status/route.ts | 21 ++++++++++++++----- .../src/lib/status/fetchPortalStatus.ts | 15 +++++++++---- apps/app-portal/src/lib/status/machine.ts | 10 +++------ 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/apps/app-portal/src/app/api/v1/status/route.ts b/apps/app-portal/src/app/api/v1/status/route.ts index 1021b11b..3e38c0de 100644 --- a/apps/app-portal/src/app/api/v1/status/route.ts +++ b/apps/app-portal/src/app/api/v1/status/route.ts @@ -1,12 +1,23 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import { decisionDates } from "../../../../lib/status/mock-singletons"; import { returnDashboardBranch } from "../../../../lib/status/machine"; import { getApplicantStatus } from "../../../../lib/status/service"; -export async function GET() { - const status = await getApplicantStatus("mock-user"); +// TODO: gate with requireUser() once Ticket 1 ships its helpers +export async function GET(req: NextRequest) { + const userId = req.nextUrl.searchParams.get("userId") ?? "mock-user"; + const status = await getApplicantStatus(userId); const showDecision = new Date() >= decisionDates.showDecision; - const branch = returnDashboardBranch(status, decisionDates, showDecision); + + const branch = returnDashboardBranch({ + user: status, + dates: { + registrationOpen: decisionDates.registrationOpen, + confirmBy: decisionDates.confirmBy, + }, + showDecision, + now: new Date(), + }); return NextResponse.json({ branch, @@ -21,4 +32,4 @@ export async function GET() { export async function POST() { return NextResponse.json({ error: "error 501" }, { status: 501 }); -} +} \ No newline at end of file diff --git a/apps/app-portal/src/lib/status/fetchPortalStatus.ts b/apps/app-portal/src/lib/status/fetchPortalStatus.ts index 8130c362..4bc60717 100644 --- a/apps/app-portal/src/lib/status/fetchPortalStatus.ts +++ b/apps/app-portal/src/lib/status/fetchPortalStatus.ts @@ -10,8 +10,6 @@ function getBaseUrl(): string { const forwardedHost = headerList.get("x-forwarded-host"); const host = forwardedHost ?? headerList.get("host"); - // If headers are not available (e.g. some dev environments), fall back - // to localhost so server-side fetches still work during local dev. if (!host) { return `http://localhost:3000`; } @@ -35,7 +33,16 @@ export async function fetchPortalStatus(): Promise { // a network fetch to the same server may fail. const status = await getApplicantStatus("mock-user"); const showDecision = new Date() >= decisionDates.showDecision; - const branch = returnDashboardBranch(status, decisionDates, showDecision); + + const branch = returnDashboardBranch({ + user: status, + dates: { + registrationOpen: decisionDates.registrationOpen, + confirmBy: decisionDates.confirmBy, + }, + showDecision, + now: new Date(), + }); return { branch, @@ -47,4 +54,4 @@ export async function fetchPortalStatus(): Promise { }, }; } -} +} \ No newline at end of file diff --git a/apps/app-portal/src/lib/status/machine.ts b/apps/app-portal/src/lib/status/machine.ts index 81d4474f..28e54e0c 100644 --- a/apps/app-portal/src/lib/status/machine.ts +++ b/apps/app-portal/src/lib/status/machine.ts @@ -1,11 +1,7 @@ -import type { ApplicantStatus, DashboardBranch, DecisionDates } from "./types"; +import type { DashboardBranch, MachineInput } from "./types"; -export function returnDashboardBranch( - user: ApplicantStatus, - dates: DecisionDates, - showDecision: boolean, -): DashboardBranch { - const now = new Date(); +export function returnDashboardBranch(input: MachineInput): DashboardBranch { + const { user, dates, showDecision, now } = input; if (now < dates.registrationOpen) { return "pre-registration"; From f9a24a55358d3b1f85d222c3ee881dc0866f165a Mon Sep 17 00:00:00 2001 From: Shreeya Adhikari Date: Wed, 1 Jul 2026 16:11:01 -0400 Subject: [PATCH 4/4] fix type alignment with user.ts across dashboard components --- apps/app-portal/jest.config.ts | 11 +++ .../src/app/(applicant)/rsvp/page.tsx | 2 +- .../src/components/dashboard/AdmittedView.tsx | 4 +- .../components/dashboard/InProgressView.tsx | 2 +- .../components/dashboard/SubmittedView.tsx | 2 +- .../src/lib/status/fetchPortalStatus.ts | 2 - .../app-portal/src/lib/status/machine.test.ts | 86 +++++++++++++++++++ .../src/lib/status/mock-singletons.ts | 2 +- apps/app-portal/src/lib/status/service.ts | 68 ++++++++++++--- apps/app-portal/src/lib/status/types.ts | 19 ++-- apps/app-portal/tsconfig.json | 5 +- 11 files changed, 175 insertions(+), 28 deletions(-) create mode 100644 apps/app-portal/jest.config.ts create mode 100644 apps/app-portal/src/lib/status/machine.test.ts diff --git a/apps/app-portal/jest.config.ts b/apps/app-portal/jest.config.ts new file mode 100644 index 00000000..75254737 --- /dev/null +++ b/apps/app-portal/jest.config.ts @@ -0,0 +1,11 @@ +import type { Config } from "jest"; + +const config: Config = { + preset: "ts-jest", + testEnvironment: "node", + moduleNameMapper: { + "^@/(.*)$": "/src/$1", + }, +}; + +export default config; \ No newline at end of file diff --git a/apps/app-portal/src/app/(applicant)/rsvp/page.tsx b/apps/app-portal/src/app/(applicant)/rsvp/page.tsx index d5ea5388..4bb55331 100644 --- a/apps/app-portal/src/app/(applicant)/rsvp/page.tsx +++ b/apps/app-portal/src/app/(applicant)/rsvp/page.tsx @@ -14,7 +14,7 @@ export default async function RsvpPage(): Promise { return ( ); diff --git a/apps/app-portal/src/components/dashboard/AdmittedView.tsx b/apps/app-portal/src/components/dashboard/AdmittedView.tsx index c749dc61..dd687269 100644 --- a/apps/app-portal/src/components/dashboard/AdmittedView.tsx +++ b/apps/app-portal/src/components/dashboard/AdmittedView.tsx @@ -55,10 +55,10 @@ export default function AdmittedView({ RSVP status

- {status.rsvpStatus === "submitted" ? "Confirmed" : "Needs RSVP"} + {status.rsvpStatus === "confirmed" ? "Confirmed" : "Needs RSVP"}

- {status.rsvpStatus === "submitted" + {status.rsvpStatus === "confirmed" ? "Thanks for confirming your attendance." : "Please complete the RSVP form before the deadline."}

diff --git a/apps/app-portal/src/components/dashboard/InProgressView.tsx b/apps/app-portal/src/components/dashboard/InProgressView.tsx index db178c2e..fac55393 100644 --- a/apps/app-portal/src/components/dashboard/InProgressView.tsx +++ b/apps/app-portal/src/components/dashboard/InProgressView.tsx @@ -16,7 +16,7 @@ type InProgressViewProps = { export default function InProgressView({ status, }: InProgressViewProps): JSX.Element { - const progressPercent = status.applicationStatus === "in-progress" ? 60 : 100; + const progressPercent = status.applicationStatus === "incomplete" ? 60 : 100; return (

- {status.rsvpStatus === "submitted" + {status.rsvpStatus === "confirmed" ? "You've already completed post-acceptance RSVP steps." : "Your application is ready for the next review stage."}

diff --git a/apps/app-portal/src/lib/status/fetchPortalStatus.ts b/apps/app-portal/src/lib/status/fetchPortalStatus.ts index 4bc60717..83421ae7 100644 --- a/apps/app-portal/src/lib/status/fetchPortalStatus.ts +++ b/apps/app-portal/src/lib/status/fetchPortalStatus.ts @@ -29,8 +29,6 @@ export async function fetchPortalStatus(): Promise { return (await response.json()) as PortalStatusResponse; } catch { - // Fallback to local in-memory service for dev environments where - // a network fetch to the same server may fail. const status = await getApplicantStatus("mock-user"); const showDecision = new Date() >= decisionDates.showDecision; diff --git a/apps/app-portal/src/lib/status/machine.test.ts b/apps/app-portal/src/lib/status/machine.test.ts new file mode 100644 index 00000000..36df9001 --- /dev/null +++ b/apps/app-portal/src/lib/status/machine.test.ts @@ -0,0 +1,86 @@ +import { returnDashboardBranch } from "./machine"; +import type { ApplicantStatus, MachineInput } from "./types"; + +const PAST = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); +const FUTURE = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); + +function makeInput(overrides: { + user?: Partial; + dates?: { registrationOpen: Date; confirmBy: Date }; + showDecision?: boolean; + now?: Date; +} = {}): MachineInput { + return { + user: { + userId: "test-user", + applicationStatus: "not-started", + rsvpStatus: "unconfirmed", + ...overrides.user, + }, + dates: overrides.dates ?? { + registrationOpen: PAST, + confirmBy: FUTURE, + }, + showDecision: overrides.showDecision ?? true, + now: overrides.now ?? new Date(), + }; +} +describe("returnDashboardBranch", () => { + test("pre-registration: now < registrationOpen regardless of other fields", () => { + const input = makeInput({ + user: { applicationStatus: "submitted", decisionStatus: "admitted" }, + dates: { registrationOpen: FUTURE, confirmBy: FUTURE }, + }); + expect(returnDashboardBranch(input)).toBe("pre-registration"); + }); + + test("in-progress: applicationStatus incomplete and now >= registrationOpen", () => { + const input = makeInput({ user: { applicationStatus: "incomplete" } }); + expect(returnDashboardBranch(input)).toBe("in-progress"); + }); + + test("not-started after open returns in-progress", () => { + const input = makeInput({ user: { applicationStatus: "not-started" } }); + expect(returnDashboardBranch(input)).toBe("in-progress"); + }); + + test("submitted with decisions hidden returns submitted", () => { + const input = makeInput({ + user: { applicationStatus: "submitted", decisionStatus: "admitted" }, + showDecision: false, + }); + expect(returnDashboardBranch(input)).toBe("submitted"); + }); + + test("submitted with decisionStatus pending returns submitted", () => { + const input = makeInput({ + user: { applicationStatus: "submitted", decisionStatus: "pending" }, + showDecision: true, + }); + expect(returnDashboardBranch(input)).toBe("submitted"); + }); + + test("admitted: submitted + admitted + showDecision true", () => { + const input = makeInput({ + user: { applicationStatus: "submitted", decisionStatus: "admitted" }, + showDecision: true, + }); + expect(returnDashboardBranch(input)).toBe("admitted"); + }); + + test("waitlisted: submitted + waitlisted + showDecision true", () => { + const input = makeInput({ + user: { applicationStatus: "submitted", decisionStatus: "waitlisted" }, + showDecision: true, + }); + expect(returnDashboardBranch(input)).toBe("waitlisted"); + }); + + test("declined: submitted + declined + showDecision true", () => { + const input = makeInput({ + user: { applicationStatus: "submitted", decisionStatus: "declined" }, + showDecision: true, + }); + expect(returnDashboardBranch(input)).toBe("declined"); + }); +}); \ No newline at end of file diff --git a/apps/app-portal/src/lib/status/mock-singletons.ts b/apps/app-portal/src/lib/status/mock-singletons.ts index a26c9e9e..b77c0ea0 100644 --- a/apps/app-portal/src/lib/status/mock-singletons.ts +++ b/apps/app-portal/src/lib/status/mock-singletons.ts @@ -77,7 +77,7 @@ export const mockApplicantStatus: ApplicantStatus = { userId: "mock-user", applicationStatus: "submitted", decisionStatus: "admitted", - rsvpStatus: "not-submitted", + rsvpStatus: "unconfirmed", }; export const decisionDates: DecisionDates = { diff --git a/apps/app-portal/src/lib/status/service.ts b/apps/app-portal/src/lib/status/service.ts index 38952fba..3cad24d5 100644 --- a/apps/app-portal/src/lib/status/service.ts +++ b/apps/app-portal/src/lib/status/service.ts @@ -1,11 +1,59 @@ -import { mockApplicantStatus } from "./mock-singletons"; -import type { ApplicantStatus, RsvpPayload, RsvpStatus } from "./types"; +import { getDb } from "@/lib/db"; +import { decisionDates, mockApplicantStatus } from "./mock-singletons"; +import { returnDashboardBranch } from "./machine"; +import type { + ApplicantStatus, + PortalStatusResponse, + RsvpPayload, + RsvpStatus, +} from "./types"; -export async function getApplicantStatus( - userId: string, -): Promise { - void userId; - return { ...mockApplicantStatus }; +export async function getApplicantStatus(userId: string): Promise { + const db = await getDb(); + const doc = await db.collection("applicant_data").findOne({ userId }); + + if (!doc) { + return { + userId, + applicationStatus: "not-started", + decisionStatus: undefined, + rsvpStatus: "unconfirmed", + }; + } + + return { + userId, + applicationStatus: doc.applicationStatus ?? "not-started", + decisionStatus: doc.decisionStatus, + rsvpStatus: doc.rsvpStatus ?? "unconfirmed", + }; +} + +export async function getPortalStatus(userId: string): Promise { + const user = await getApplicantStatus(userId); + + // TODO: replace with getSingleton() calls once Ticket 5 lands real singleton service + const showDecision = new Date() >= decisionDates.showDecision; + + const branch = returnDashboardBranch({ + user, + dates: { + registrationOpen: decisionDates.registrationOpen, + confirmBy: decisionDates.confirmBy, + }, + showDecision, + now: new Date(), + }); + + return { + branch, + status: user, + decisionDates: { + registrationOpen: decisionDates.registrationOpen.toISOString(), + confirmBy: decisionDates.confirmBy.toISOString(), + showDecision: decisionDates.showDecision.toISOString(), + }, + }; } export async function saveRsvp( @@ -14,6 +62,6 @@ export async function saveRsvp( ): Promise { void userId; void payload; - mockApplicantStatus.rsvpStatus = "submitted"; - return "submitted"; -} + mockApplicantStatus.rsvpStatus = "confirmed"; + return "confirmed"; +} \ No newline at end of file diff --git a/apps/app-portal/src/lib/status/types.ts b/apps/app-portal/src/lib/status/types.ts index 7efd077e..185fd0ba 100644 --- a/apps/app-portal/src/lib/status/types.ts +++ b/apps/app-portal/src/lib/status/types.ts @@ -1,3 +1,11 @@ +import type { + ApplicationStatus, + DecisionStatus, + RsvpStatus, +} from "@/lib/types/user"; + +export type { ApplicationStatus, DecisionStatus, RsvpStatus }; + export type DashboardBranch = | "pre-registration" | "in-progress" @@ -11,7 +19,8 @@ export type ApplicantStatus = { applicationStatus: ApplicationStatus; decisionStatus?: DecisionStatus; rsvpStatus: RsvpStatus; -} +}; + export type MachineInput = { user: ApplicantStatus; dates: { @@ -22,12 +31,6 @@ export type MachineInput = { now: Date; }; -export type ApplicationStatus = "in-progress" | "submitted"; - -export type DecisionStatus = "pending" | "admitted" | "waitlisted" | "declined"; - -export type RsvpStatus = "not-submitted" | "submitted"; - export type RsvpPayload = { attending: "yes" | "no"; dietaryRestrictions: string; @@ -52,4 +55,4 @@ export type PortalStatusResponse = { branch: DashboardBranch; status: ApplicantStatus; decisionDates: SerializedDecisionDates; -}; +}; \ No newline at end of file diff --git a/apps/app-portal/tsconfig.json b/apps/app-portal/tsconfig.json index 24b8cfcd..c8c03b48 100644 --- a/apps/app-portal/tsconfig.json +++ b/apps/app-portal/tsconfig.json @@ -5,7 +5,8 @@ "paths": { "@/*": ["./src/*"], "@util/*": ["../../packages/util/src/*"] - } + }, + "types": ["jest"] }, "include": [ "next-env.d.ts", @@ -15,4 +16,4 @@ "../../packages/ui/src/Placeholder.tsx" ], "exclude": ["node_modules"] -} +} \ No newline at end of file