Skip to content

Admin Settings — date singleton read/write #485

@adityapat24

Description

@adityapat24

Goal

The three date singletons (registration-open, registration-closed, confirm-by) are stored in Mongo and editable from the admin settings UI. When an admin saves a new date, the value persists.

Tasks

Define the singleton data shape

  • Confirm the schema for the singleton_data collection: one document per key, shape { _id: string, key: SingletonKey, value: unknown, updatedAt: Date, updatedBy: string }.
    • What this accomplishes: Each singleton key gets its own document. Using the key as _id makes lookups trivial and uniqueness automatic.
  • For date keys, value is an ISO 8601 string (e.g., "2026-02-15T18:00:00.000Z"). Document this in a comment.
    • What this accomplishes: One consistent date format across the app. ISO strings are unambiguous about timezone, sort lexicographically, and serialize cleanly through JSON.

Implement the singleton service

  • In src/lib/admin/singleton-service.ts, write getSingleton<K extends SingletonKey>(key: K): Promise<SingletonValue<K> | null>.
    • What this accomplishes: A type-safe function any feature can use to read any singleton. The generic ensures getSingleton('confirm-by') returns a string-typed Date, while getSingleton('show-decision') returns a boolean.
  • Query singleton_data by _id === key. If found, return doc.value. If not, return null.
    • What this accomplishes: Returns null when never set, so callers can decide on a default (the UI shows "not set" and won't crash).
  • Write setSingleton<K extends SingletonKey>(key: K, value: SingletonValue<K>, updatedBy: string): Promise<void>.
    • What this accomplishes: A single function for writing any singleton. Recording who updated it is useful for audit purposes.
  • Use updateOne({ _id: key }, { $set: { key, value, updatedAt: new Date(), updatedBy } }, { upsert: true }).
    • What this accomplishes: Upsert means it creates on first set and updates on subsequent sets. No two-code-path branching.

Validate date values before writing

  • In the same file, write validateDateSingleton(value: unknown): { ok: true, value: string } | { ok: false, error: string }. Check the value is a string, parses to a valid Date, and is in ISO 8601 format.
    • What this accomplishes: Stops garbage from being saved. An admin typo could otherwise leave the system in a state where every other feature crashes on date parsing.
  • If valid, normalize to ISO by re-formatting: new Date(value).toISOString().
    • What this accomplishes: Consumers can rely on a single canonical format regardless of what the admin's input field produces.

Implement the three date endpoints

  • For each of src/app/api/v1/dates/registration-open/route.ts, src/app/api/v1/dates/registration-closed/route.ts, src/app/api/v1/dates/confirm-by/route.ts:
    • Implement GET: call getSingleton('<key>'), return { value } or { value: null }.
      • What this accomplishes: Any authed user (applicants, admins) can read these. The frontend dashboard already calls these endpoints.
    • Implement POST: parse { value } from the body, validate with validateDateSingleton, call setSingleton with the validated value and the current admin's email as updatedBy, return { ok: true, value }. On validation failure return 400 with the error.
      • What this accomplishes: The admin settings page can save new dates.
  • Add a comment to each POST: // TODO: gate with requireAdmin() once Ticket 1 ships its helpers. For updatedBy, use a placeholder (query string or 'unknown') until then.

Cross-cutting consistency

  • Make sure all three endpoints return the same response shape: { value: string | null } for GET, { ok: true, value: string } or { ok: false, error: string } for POST.
    • What this accomplishes: Frontend code can use one fetch wrapper for all three. Less branching, fewer bugs.

Test all three endpoints via curl

  • curl localhost:3000/api/v1/dates/registration-open should return { value: null } initially.
  • curl -X POST localhost:3000/api/v1/dates/registration-open -d '{"value":"2026-02-15T18:00:00Z"}' should return { ok: true, value: "2026-02-15T18:00:00.000Z" }.
  • curl localhost:3000/api/v1/dates/registration-open should now return { value: "2026-02-15T18:00:00.000Z" }.
  • curl -X POST localhost:3000/api/v1/dates/registration-open -d '{"value":"not a date"}' should return 400 with an error.
  • Repeat for registration-closed and confirm-by to confirm all three work independently.
    • What this accomplishes: Proves each endpoint works without depending on the others. Catches per-endpoint regressions.

Test from the admin settings page

  • Start the dev server, go to /admin/settings, change a date in the UI, click save. Confirm the page shows success state and the value sticks after reload.
    • What this accomplishes: Proves the round trip through the real frontend. The frontend was built against mocks; this confirms it works against real persistence.
  • Change all three dates. Confirm each persists independently.

Unit tests

  • Test validateDateSingleton accepts valid ISO strings, rejects garbage, rejects non-string inputs, rejects dates that don't parse.
  • Test getSingleton returns null for missing key, returns the value for a present key.
  • Test setSingleton followed by getSingleton returns the written value.
    • What this accomplishes: Locks in the read-write round trip and the validation rules.

Definition of done

  • An admin can change any of the three dates on /admin/settings and the value persists.
  • Each endpoint returns the right shape for GET and POST, including the 400 error path.
  • Unit tests pass for validation and round-trip.
  • Documents in singleton_data collection have the expected key, value, updatedAt, updatedBy fields.

Metadata

Metadata

No fields configured for Feature.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions