diff --git a/apps/app-portal/src/app/api/v1/registration/route.ts b/apps/app-portal/src/app/api/v1/registration/route.ts index 3775511a..275c4d5e 100644 --- a/apps/app-portal/src/app/api/v1/registration/route.ts +++ b/apps/app-portal/src/app/api/v1/registration/route.ts @@ -1,18 +1,44 @@ -import { NextResponse } from "next/server"; +import { type NextRequest, NextResponse } from "next/server"; -import { getRegistrationState } from "@/lib/application/service"; +import { + getDraft, + getRegistrationState, + saveDraft, +} from "@/lib/application/service"; +import type { ApplicationResponses } from "@/lib/application/types"; -export async function GET() { +// TODO: replace with session lookup once auth helpers exist (sprint 4) +function getUserId(req: NextRequest): string | null { + return req.headers.get("x-user-id") ?? req.nextUrl.searchParams.get("userId"); +} + +export async function GET(req: NextRequest) { const state = await getRegistrationState(); + const userId = getUserId(req); + if (userId) { + const draft = await getDraft(userId); + if (draft) { + return NextResponse.json({ + ...state, + responses: draft.responses, + updatedAt: draft.updatedAt, + }); + } + } return NextResponse.json(state); } -export async function POST() { - // save submission - return NextResponse.json({ error: "Not implemented" }, { status: 501 }); +export async function POST(req: NextRequest) { + const userId = getUserId(req); + if (!userId) { + return NextResponse.json({ error: "Missing userId" }, { status: 400 }); + } + const body = (await req.json()) as { responses: ApplicationResponses }; + const draft = await saveDraft(userId, body.responses); + return NextResponse.json({ ok: true, savedAt: draft.updatedAt }); } export async function PUT() { - // submits application + // submits application — wired in a future ticket return NextResponse.json({ error: "Not implemented" }, { status: 501 }); } diff --git a/apps/app-portal/src/components/application/ApplicationForm.tsx b/apps/app-portal/src/components/application/ApplicationForm.tsx index d064b34b..378f12a7 100644 --- a/apps/app-portal/src/components/application/ApplicationForm.tsx +++ b/apps/app-portal/src/components/application/ApplicationForm.tsx @@ -30,7 +30,7 @@ import type { import { FormSection } from "./FormSection"; -const REGISTRATION_API = "/api/v1/registration"; +const REGISTRATION_API = "/api/v1/registration?userId=test-user-1"; const AUTOSAVE_DELAY_MS = 2000; export function ApplicationForm() { diff --git a/apps/app-portal/src/components/application/ShortTextField.tsx b/apps/app-portal/src/components/application/ShortTextField.tsx index 2a9b56b2..b224bb84 100644 --- a/apps/app-portal/src/components/application/ShortTextField.tsx +++ b/apps/app-portal/src/components/application/ShortTextField.tsx @@ -30,6 +30,7 @@ export function ShortTextField({ disabled={disabled} placeholder={question.label} aria-required={question.required} + required={question.required} /> ); } diff --git a/apps/app-portal/src/lib/application/service.ts b/apps/app-portal/src/lib/application/service.ts index aaff2a9f..2fa54d68 100644 --- a/apps/app-portal/src/lib/application/service.ts +++ b/apps/app-portal/src/lib/application/service.ts @@ -1,3 +1,4 @@ +import { getDb } from "@/lib/db"; import type { ApplicationDraft, ApplicationResponses, @@ -5,6 +6,20 @@ import type { RegistrationState, } from "./types"; +/* + * Mongo collection: applicant_data + * + * Document shape: + * userId: string — unique per applicant + * applicationResponses: Record + * applicationStatus: 'in-progress' | 'submitted' + * lastSavedAt: Date + * appSubmissionTime: Date | null — null until submitted + * + * Requires env vars: MONGO_PROD_CONNECTION_STRING, MONGO_SERVER_DBNAME + */ +const COLLECTION = "applicant_data"; + // Change these values to test every application state end-to-end. // registrationStatus: "before_open" | "open" | "closed" // applicationStatus: "draft" | "submitted" @@ -27,25 +42,40 @@ export async function isRegistrationOpen(): Promise { return state.registrationStatus === "open"; } -/** @todo Persist draft to MongoDB */ export async function getDraft( userId: string, ): Promise { - void userId; - return null; + const db = await getDb(); + const doc = await db.collection(COLLECTION).findOne({ userId }); + if (!doc) return null; + return { + responses: doc.applicationResponses as ApplicationResponses, + updatedAt: (doc.lastSavedAt as Date).toISOString(), + status: "draft", + }; } -/** @todo Persist draft to MongoDB */ export async function saveDraft( userId: string, responses: ApplicationResponses, ): Promise { - void userId; - return { - responses, - updatedAt: new Date().toISOString(), - status: "draft", - }; + const db = await getDb(); + const now = new Date(); + await db.collection(COLLECTION).updateOne( + { userId }, + { + $set: { applicationResponses: responses, lastSavedAt: now }, + // $setOnInsert never overwrites an existing applicationStatus, + // which prevents a draft save from downgrading a submitted application. + $setOnInsert: { + userId, + applicationStatus: "in-progress", + appSubmissionTime: null, + }, + }, + { upsert: true }, + ); + return { responses, updatedAt: now.toISOString(), status: "draft" }; } /** @todo Persist submission to MongoDB */ diff --git a/apps/app-portal/src/lib/db.ts b/apps/app-portal/src/lib/db.ts index 76ad836a..d2bc1e0b 100644 --- a/apps/app-portal/src/lib/db.ts +++ b/apps/app-portal/src/lib/db.ts @@ -20,16 +20,25 @@ declare global { var __mongoClientPromise__: Promise | undefined; } -const client = new MongoClient(uri); -const clientPromise = global.__mongoClientPromise__ ?? client.connect(); - -if (process.env.NODE_ENV !== "production") { - global.__mongoClientPromise__ = clientPromise; +// Lazily creates and caches the connection so importing this module +// doesn't throw when MONGO_PROD_CONNECTION_STRING is absent (e.g. in dev +// without a local Mongo instance). The error surfaces only when getDb() is called. +function getClientPromise(uri: string): Promise { + if (global.__mongoClientPromise__) return global.__mongoClientPromise__; + const promise = new MongoClient(uri).connect(); + if (process.env.NODE_ENV !== "production") { + global.__mongoClientPromise__ = promise; + } + return promise; } export async function getDb(): Promise { - const connectedClient = await clientPromise; - return connectedClient.db(dbName); + const uri = process.env.MONGO_PROD_CONNECTION_STRING; + const dbName = process.env.MONGO_SERVER_DBNAME; + if (!uri) throw new Error("Missing MONGO_PROD_CONNECTION_STRING"); + if (!dbName) throw new Error("Missing MONGO_SERVER_DBNAME"); + const client = await getClientPromise(uri); + return client.db(dbName); } /**