Provinces, districts, municipalities — three tiers of one hierarchy.
Primary user value
Gives every person address and the school's own address a normalised place to point at, so a pupil's district can be aggregated nationally instead of read back as free text.
Actors
Admin only — no other role is verified to hold any Geography_* permission.
The seed carries all 7 provinces and all 77 districts, but only 36 of Nepal's 753 municipalities — see Backend §5.4. A school outside those 36 adds its own local level from the Classifications screen the first time it needs to record an address there.
Renaming or reparenting a province, district or municipality that a person's address, the school profile, or a child row already points at is refused outright, never merely warned about. Retirement (isActive: false) stays available regardless, because it does not touch what the name says.
Hard delete is permitted only when nothing references the row. This is an application pre-check over an enumerated set of referencing columns, not a caught database error.
Retirement must actually remove a row from a form's pick list, or retiring it means nothing to the person filling the form.
An admin managing retired rows must explicitly ask for them.
findAll in all three services, includeInactive query flag
Rows with isActive: false are omitted unless includeInactive=true
Schema docblock; service findAll
Renaming or reparenting a referenced row is refused
The export writes place NAMES rather than surrogate ids, so a rename retroactively rewrites the content of every export already downloaded and repoints every future name-resolved import.
An admin correcting a name must retire and re-add rather than edit in place.
isReferenced pre-check in all three update methods
409 GEOGRAPHY_RENAME_REFERENCED
Service tests
Delete is an application pre-check, not a caught constraint
ON DELETE RESTRICT raises 23001, not the 23503 a naive translator would expect — a caught-code mapping would have matched the wrong SQLSTATE entirely.
An admin deleting a referenced row gets a named, actionable error instead of an unmapped 500.
isReferenced pre-check in all three delete methods, enumerated column list
409 GEOGRAPHY_IN_USE
Provinces service docblock, probed this session
A district/municipality needs its parent to write a fill-order-consistent address
A composite foreign key is MATCH SIMPLE — it does nothing when any column is NULL — so the CHECK constraints on the address columns are what make a half-filled address representable and a wrong one impossible.
An address form cannot select a district without its province being set first.
CHECK constraints on users and school_profile, not on this module's own tables
ADDRESS_HIERARCHY_INVALID from the writing module (people, school-profile)
geography.ts schema docblock
Composite FK targets are unique(...) not uniqueIndex(...)
drizzle-kit emits every foreign key before every index, so an index-form target does not exist yet when the referencing key is added, and the migration dies with 42830 on a clean database.
None directly — an implementation detail that keeps migrations applying cleanly.
geography.ts schema docblock, recorded against designations_department_id_id_unique
No deleted_at on any of the three tables
A soft delete never fires ON DELETE RESTRICT, so a soft-deleted district would keep resolving for every address still pointing at it while vanishing from the admin's list.
Retirement (isActive=false) is the only "remove from view" action; delete is destructive and checked.