Skip to content

Application Form — draft save & load #483

@adityapat24

Description

@adityapat24

Goal

Autosave from the application form persists to Mongo. Reloading the form pulls back the saved draft and rehydrates the inputs.

Tasks

Set up the Mongo data shape

  • Decide on the document shape for applicant_data. Each applicant has one document keyed by userId (or email). The shape should include at minimum: userId, email, applicationResponses (an object whose keys are question IDs), applicationStatus ('not-started' | 'in-progress' | 'submitted'), lastSavedAt (Date), and appSubmissionTime (Date, null until submitted).
    • What this accomplishes: Locks in the schema so the service layer has a clear contract. Picking the shape upfront prevents back-and-forth refactors later.
  • Document the shape in a comment at the top of src/lib/application/service.ts.
    • What this accomplishes: The next person reading the service knows exactly what's in Mongo without having to query it.

Implement getDraft(userId)

  • Add getDraft(userId: string): Promise<ApplicationDraft | null> to src/lib/application/service.ts.
    • What this accomplishes: A single function the API layer can call to fetch a user's progress. Returning null when there's no document means the form knows to render empty.
  • Use the Mongo client from src/lib/db.ts to query applicant_data by userId.
    • What this accomplishes: Reuses the singleton client instead of opening new connections per call.
  • If no document exists, return null. If one exists, return { responses: doc.applicationResponses, status: doc.applicationStatus, lastSavedAt: doc.lastSavedAt }.
    • What this accomplishes: Returns a consistent shape regardless of whether the user has started yet.

Implement saveDraft(userId, payload)

  • Add saveDraft(userId: string, payload: { responses: Record<string, unknown> }): Promise<void> to the same service file.
    • What this accomplishes: A single function for "write the user's current form state to the database."
  • Use Mongo's updateOne with upsert: true. Set applicationResponses from the payload, set applicationStatus to 'in-progress' (only if it isn't already 'submitted'), set lastSavedAt to new Date(). On insert, also set userId and email.
    • What this accomplishes: Upsert means it creates the document on first save and updates it on subsequent saves, without needing two code paths.
  • Don't overwrite applicationStatus if it's already 'submitted'. Use $set for the responses and lastSavedAt, but use $setOnInsert for any field that shouldn't be touched after the first write.
    • What this accomplishes: Prevents a draft save from accidentally reverting a submitted application back to in-progress.

Wire the API endpoints

  • Update src/app/api/v1/registration/route.ts. Implement the GET handler: read userId from the query string (or from a placeholder header — auth wiring comes in sprint 4), call getDraft, return JSON.
    • What this accomplishes: The endpoint the frontend autosave hook already calls now returns real data.
  • Implement the POST handler: read userId from query string, read payload from the request body, call saveDraft, return { ok: true, savedAt: <ISO timestamp> }.
    • What this accomplishes: The autosave actually persists. The returned timestamp lets the frontend show "saved X seconds ago" accurately.
  • Leave the PUT handler returning 501 for now — submit is a future ticket.
    • What this accomplishes: Makes it clear what's implemented and what isn't.
  • Use a hardcoded userId source for now (a ?userId= query param or x-user-id header). Add a comment // TODO: replace with session lookup once Ticket 1's helpers exist.
    • What this accomplishes: Lets you ship the data layer without being blocked on the session helpers. The swap to real auth in sprint 4 is a 3-line change.

Test with the running frontend

  • Start the dev server, sign in (or fake a userId), load /application, type into a field, wait for autosave to fire, refresh the page, confirm the typed value is still there.
    • What this accomplishes: End-to-end proof that save and load work together against the real frontend.
  • Repeat with multiple fields and section navigation.
    • What this accomplishes: Catches bugs like "only the last field saves" or "section state isn't included in the payload."

Unit tests

  • Write a test that calls getDraft against a freshly seeded user with no document, expects null.
  • Write a test that calls saveDraft then getDraft, expects to read back what was written.
  • Write a test that confirms saveDraft doesn't downgrade 'submitted' back to 'in-progress'.
    • What this accomplishes: Locks in the critical invariants. The third test in particular prevents a regression that would silently corrupt submitted applications.

Definition of done

  • A user types into the form, autosave fires, the data lands in Mongo.
  • Refreshing the page reloads the saved draft.
  • Closing the browser and returning days later still shows the draft.
  • Three unit tests pass for getDraft, saveDraft, and the no-downgrade rule.

Metadata

Metadata

Assignees

No fields configured for Feature.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions