You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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).
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.
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
singleton_datacollection: one document per key, shape{ _id: string, key: SingletonKey, value: unknown, updatedAt: Date, updatedBy: string }._idmakes lookups trivial and uniqueness automatic.valueis an ISO 8601 string (e.g.,"2026-02-15T18:00:00.000Z"). Document this in a comment.Implement the singleton service
src/lib/admin/singleton-service.ts, writegetSingleton<K extends SingletonKey>(key: K): Promise<SingletonValue<K> | null>.getSingleton('confirm-by')returns a string-typed Date, whilegetSingleton('show-decision')returns a boolean.singleton_databy_id === key. If found, returndoc.value. If not, return null.setSingleton<K extends SingletonKey>(key: K, value: SingletonValue<K>, updatedBy: string): Promise<void>.updateOne({ _id: key }, { $set: { key, value, updatedAt: new Date(), updatedBy } }, { upsert: true }).Validate date values before writing
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.new Date(value).toISOString().Implement the three date endpoints
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:GET: callgetSingleton('<key>'), return{ value }or{ value: null }.POST: parse{ value }from the body, validate withvalidateDateSingleton, callsetSingletonwith the validated value and the current admin's email asupdatedBy, return{ ok: true, value }. On validation failure return 400 with the error.POST:// TODO: gate with requireAdmin() once Ticket 1 ships its helpers. ForupdatedBy, use a placeholder (query string or'unknown') until then.Cross-cutting consistency
{ value: string | null }for GET,{ ok: true, value: string }or{ ok: false, error: string }for POST.Test all three endpoints via curl
curl localhost:3000/api/v1/dates/registration-openshould 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-openshould 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.registration-closedandconfirm-byto confirm all three work independently.Test from the admin settings page
/admin/settings, change a date in the UI, click save. Confirm the page shows success state and the value sticks after reload.Unit tests
validateDateSingletonaccepts valid ISO strings, rejects garbage, rejects non-string inputs, rejects dates that don't parse.getSingletonreturns null for missing key, returns the value for a present key.setSingletonfollowed bygetSingletonreturns the written value.Definition of done
/admin/settingsand the value persists.singleton_datacollection have the expectedkey,value,updatedAt,updatedByfields.