Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 33 additions & 7 deletions apps/app-portal/src/app/api/v1/registration/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export function ShortTextField({
disabled={disabled}
placeholder={question.label}
aria-required={question.required}
required={question.required}
/>
);
}
50 changes: 40 additions & 10 deletions apps/app-portal/src/lib/application/service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import { getDb } from "@/lib/db";
import type {
ApplicationDraft,
ApplicationResponses,
ApplicationSubmission,
RegistrationState,
} from "./types";

/*
* Mongo collection: applicant_data
*
* Document shape:
* userId: string — unique per applicant
* applicationResponses: Record<string, string | string[] | null>
* 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"
Expand All @@ -27,25 +42,40 @@ export async function isRegistrationOpen(): Promise<boolean> {
return state.registrationStatus === "open";
}

/** @todo Persist draft to MongoDB */
export async function getDraft(
userId: string,
): Promise<ApplicationDraft | null> {
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<ApplicationDraft> {
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 */
Expand Down
23 changes: 16 additions & 7 deletions apps/app-portal/src/lib/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,25 @@ declare global {
var __mongoClientPromise__: Promise<MongoClient> | 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<MongoClient> {
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<Db> {
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);
}

/**
Expand Down