feat: capture the secondary activity and the territorial location - #617
Open
mrivas00 wants to merge 22 commits into
Open
feat: capture the secondary activity and the territorial location#617mrivas00 wants to merge 22 commits into
mrivas00 wants to merge 22 commits into
Conversation
mrivas00
force-pushed
the
docs/mati/rd-branch-freeze
branch
from
August 24, 2026 12:25
0629f4f to
24da206
Compare
mrivas00
force-pushed
the
feat/mati/rd-organization-form
branch
2 times, most recently
from
August 24, 2026 15:10
0be1de0 to
ecd8724
Compare
mrivas00
force-pushed
the
docs/mati/rd-branch-freeze
branch
from
August 24, 2026 15:10
24da206 to
90dedd8
Compare
mrivas00
force-pushed
the
feat/mati/rd-organization-form
branch
from
August 24, 2026 17:58
ecd8724 to
c0e6b4a
Compare
mrivas00
force-pushed
the
docs/mati/rd-branch-freeze
branch
from
August 24, 2026 17:58
90dedd8 to
a46f61a
Compare
mrivas00
force-pushed
the
docs/mati/rd-branch-freeze
branch
from
August 24, 2026 19:11
a46f61a to
7bfcbc8
Compare
mrivas00
force-pushed
the
feat/mati/rd-organization-form
branch
from
August 24, 2026 19:11
c0e6b4a to
6ac727a
Compare
mrivas00
force-pushed
the
docs/mati/rd-branch-freeze
branch
from
August 25, 2026 18:50
7bfcbc8 to
3f0cade
Compare
mrivas00
force-pushed
the
feat/mati/rd-organization-form
branch
2 times, most recently
from
August 25, 2026 18:52
6647586 to
a4f3634
Compare
mrivas00
force-pushed
the
docs/mati/rd-branch-freeze
branch
from
August 26, 2026 12:12
82adb94 to
f65c708
Compare
mrivas00
force-pushed
the
feat/mati/rd-organization-form
branch
from
August 26, 2026 12:12
a368a3f to
ed205e6
Compare
mrivas00
force-pushed
the
docs/mati/rd-branch-freeze
branch
from
August 26, 2026 12:36
f65c708 to
719052a
Compare
mrivas00
force-pushed
the
feat/mati/rd-organization-form
branch
from
August 26, 2026 12:36
ed205e6 to
dceb2a7
Compare
The Dominican registry locates an organization by administrative subdivision, so the seed loads the hierarchy the form selects from. Nodes declare their own level rather than deriving it from the nesting depth, so a branch that skips a level in the official catalog loads without a code change. The base dataset carries the ten planning regions and the 32 provinces, which are stable and well established. The three levels below them wait on the ONE catalog: a partial municipality list would let a registrant find their province and not their municipality, which is worse than offering neither. The seeder needs no change to load them. The migration already loads this catalog, because `seed.ts` skips entirely once a country exists. This path is what runs on a database created without migrations, so it returns early when the table already holds rows — before reading the file, which is why the `testing` fixture is gone. There is no global count assertion either: the table may hold rows this file does not describe, and every node is already verified as it is written. Written depth by depth — two queries per level — because a child needs its parent's generated id and `createMany` returns none. That keeps the cost linear in the depth rather than in the node count, which matters once the sector level lands.
`GET /api/territories` returns the children of a node, or the roots when `parentId` is omitted, which is exactly what one dependent selector needs. The whole tree is never served: the official catalog reaches the sector level, tens of thousands of rows, and a form only ever shows the siblings under the node just picked. An empty list is a 200, not a 404 — a leaf of the hierarchy and a level the catalog has not filled in yet are both expected.
Two fields the Dominican registry asks for reach the organization profile: a secondary economic activity drawn from the same catalog as the primary one, and the innermost territorial node the registrant knows. The territorial ancestors are never stored — the response walks four nested parents, which is the whole chain at five levels deep, so the form can prefill its selectors without a recursive query or a closure table. `cloneOrganizationData` copies the two new columns too: it is what carries a profile into a re-submission, and a field it forgets is a field the user silently loses on review. The form-fields endpoint mirrors the form, so it gains the two keys, renames the representative identifier to the identity document, and picks up the terminology the rest of the deployment already uses.
The delete-warning dialog tells an admin how many organizations end up pointing at a row they are about to soft-delete. With a second reference into the activity catalog, the `_count` it was reading was understating that number: an organization whose only link to the row is its secondary activity was invisible, and deleting a sector cascades to its activities all the same. `_count` cannot express the union of two columns, so the count moves to a `groupBy` over both. Grouping rather than summing two counts is what makes an organization that declares the same activity as primary and secondary count once instead of twice — the result set is bounded by the number of distinct activity pairs, not by the number of organizations.
The secondary economic activity is one flat search over the whole catalog, labelled `Sector — Actividad`, not a second sector-plus-activity pair: a hotel with an agricultural operation has to be able to declare a secondary activity in another sector. The primary activity is filtered out of it. The territorial selectors are dependent and optional — a registrant answers as far down as they know, and only the innermost answer is stored. Picking a different ancestor clears everything below it, since a municipality does not belong to the newly chosen province. A level with no rows is not rendered at all: while the catalog below province is pending, an empty selector would be a dead control rather than a missing answer. `Dirección / Región` becomes `Dirección física`, independent of the hierarchy, and moves with the selectors into a location section of its own. The representative's identifier is renamed to the identity document and carries the note saying which one applies — cédula for Dominican nationals, passport for foreign nationals without one. The kind is not stored: the number identifies it, and nothing reports on the distinction. Also corrects two labels the terminology pass missed in the profile view: `Actividad principal` and `Dirección`.
Round trips the two new fields through create and read: a territorial answer returns its ancestors outermost first, and an undeclared one returns an empty chain. The reference-count tests are the ones that would have caught the understated delete warning: an organization reaching a sector only through its secondary activity is counted, and one that declares the same activity as both primary and secondary is counted once. The territories endpoint is asserted against the hierarchy the migration loads rather than against rows the test creates, so it also covers the parent links having been built correctly. Its assertions are structural wherever they can be, because the catalog is real data pending MMARN validation and a renamed province must not break the suite. Existing organization payloads gain the two keys: the mutation schema is strict and its fields are required-nullable, so a client has to be explicit about them.
The catalog followed the regionalization of Decreto 710-04, which the Ley Orgánica de Regiones Únicas de Planificación (núm. 345-22, G. O. 11077 del 2 de agosto de 2022) replaced — the law cites that decree among its `Visto` and supersedes it. Azua moves from Valdesia to El Valle, which is the one that mattered: Valdesia drops from four provinces to three and El Valle rises from two to three, so an organization in Azua was declaring a region the law does not assign it. The other four are wording the ministry reads when it validates the catalog: Baoruco → Bahoruco, Monte Cristi → Montecristi, Higuamo → Higüamo, and `Ozama o Metropolitana` → Ozama.
Article 7 of Ley 345-22 lists the whole upper hierarchy in one place: the ten planning regions, the provinces of each, and the municipios of each province. The municipal level was deferred on the assumption that it waited on the ONE catalog; it does not. All 157 municipios come from the law, and the ONE's División Territorial 2021 reports the same count. `seedTerritories` needs no change — it takes each node's level from the file and writes depth by depth, so this is one more round. The leaf test used the province Santiago, which now has ten children. It moves to the innermost level, resolved structurally rather than by name, and gains two cases the third level introduces: a province returning municipios, and the Distrito Nacional returning none, since it is itself the municipal level and the form must render that selector empty rather than treat the province as unanswered.
The activity catalog carries a sources document; the territorial one did not, and its commit message asserted the regions and provinces were "stable and well established" without citing anything. That is the kind of claim the activity catalog obliged itself to back. States the source (Ley 345-22, article 7), the ONE's corroborating counts, the five rows the law corrected, and why two of the five levels are deliberately empty — with the sources identified for both, since the gap is ours rather than the country's: distritos municipales exist in law and parajes in the census. Also records the modelling choices MMARN would otherwise discover on its own: the Distrito Nacional sits at the province level and carries no municipios, and the five-level shape comes from the observations rather than from the ONE, whose own hierarchy has more steps.
The official hierarchy is five levels deep, but only the levels an official source has been obtained for carry rows — today the law's three. The form has to know which those are, and asking the catalog beats a list written by hand: the hardcoded version is wrong from the moment data lands until someone remembers to edit it. `GET /api/territories/levels` returns them outermost first, ordered against the enum's declaration rather than by letting Postgres sort the enum, so the intent survives a value added in the wrong place. Public like the catalog it describes, and cached on the client for the session: territories are seed-managed reference data with no maintainer, so nothing in the app can change the answer.
A selector used to materialise once the level above it was answered, so the location section grew a field at a time and the shape of the question changed while it was being filled in. It now mirrors the sector and economic-activity pair: the level below stays on screen, disabled, and its label says which answer unlocks it. Which levels render is no longer inferred from whether the previous answer produced options — that conflated "you have not answered yet" with "this level has no data at all". It comes from the catalog, so the two levels still waiting on their source are absent rather than dead, and appear on their own the day they load. A level whose branch genuinely ends stays locked with no options: the Distrito Nacional is itself the municipal level, so its municipio selector is empty by law rather than by omission. The hook is renamed to `useTerritorySelectors`, since `useTerritoryLevels` now names the query that answers what the catalog holds.
Reordering the form left two rows pairing a field carrying `flex-1` with one that does not. A MUI field is `fullWidth`, so its flex basis is the whole row, while `flex-1` sets a basis of zero — put them side by side and there is no free space to distribute, so the `flex-1` field collapses to nothing. `Cantidad de trabajadores` was pushing the activity-unit selector down to a truncated sliver, and `Correo`, having lost its class and its spacer, was spanning the full dialog while every comparable field takes half.
Task 1.3 assumed all three levels below the province waited on the ONE. Two do. The municipal level came from article 7 of Ley 345-22, which the change had not read, and the ONE's own counts corroborate it — so the task narrows to the municipal district and the sector/paraje, and now names where those are: the IDE-RD layers, which exist but are not served anonymously. Also corrects the alternatives note in `design.md`, which counted three levels as still outstanding.
They were a wrapping row sized by a minimum width, so three levels packed into a single line while every other row in this dialog is two fields wide. They now break into rows of two, and a lone selector keeps half the width instead of spanning the dialog. Each also says it is optional once it is actionable: the chain is answered "según aplique", and nothing below the region is required. A locked selector keeps showing which answer unlocks it, where the suffix would compete with the instruction rather than add to it.
The address sat on a row of its own, so an odd number of territorial selectors left a hole above it and the address below one. It is now the last cell of the same two-column grid, which puts it beside the innermost selector whenever there is room — and moves it on its own the day a level lands, without anyone rearranging the section. `TerritorySelectors` becomes `LocationFields`, since it now lays out the whole section rather than the selectors alone.
The secondary activity sat alone with a hole beside it while the activity unit shared a row with the employee count, which reads as though the unit belongs to the headcount. Pairing the two activity fields puts the related questions together and leaves the count on its own.
The API already returned them — `getOrgSummaryDetails` includes the secondary activity and the territory with its ancestors, and the response schema carries both — but the review dialog never rendered them, so a reviewer approving an inscription could not see the location or the secondary activity the registrant declared. It also still carried two labels the terminology pass renamed everywhere else: `Actividad principal`, which names intensity denominators rather than an economic activity, and `Dirección`, which is now the physical address distinct from the hierarchy. Only the levels the organization answered are rendered, so the dialog reads what was declared rather than the shape of the catalog. The chain-building the profile view did inline becomes `getTerritoryChain`, since two screens now present the same location the same way.
The selector was enabled from the start, so an organization could declare a secondary economic activity and no primary one — incoherent, and nothing rejected it: the API persists `secondarySubsectorId` without a cross-check. It now waits for the primary, with the label naming what unlocks it, the way the primary waits for the sector. The catalog it offers is unchanged: the secondary still spans every sector, because the whole point is that a hotel with an agricultural operation declares one outside its own. It also closes a way to store the same activity twice. The options exclude whatever the primary holds, but that filter runs after the fact: pick a secondary, then set the primary to it, and the field renders empty while still submitting the value. Changing the primary now clears the secondary when the two would collide, and when the primary is cleared outright.
`useOrganizationData` builds its activity filter from a sector *and* a subsector, and the endpoint reads both — it widens the list additively, so a subsector-scoped denominator is offered alongside the sector's and the generic ones. The dialog never passed the subsector, so that half of the filter was always `undefined`. Nothing changes today: every row in the Dominican seed is scoped to a sector or to nothing. It changes the day MMARN adds a denominator for one activity — which is a data change, and would otherwise silently fail to appear while the form kept clearing the field every time the primary activity changed, as if it depended on it. `selectedActivityId` and the `selectedActivity` it resolved go: the only caller never passed the id, so the lookup could not return anything.
Every other feature in apps/api carries an integration test; this endpoint shipped without one. It decides which territorial selectors the organization form renders, so the cases that matter are that the two levels waiting on the IDE-RD layers stay out, and that the order is the hierarchy's rather than the alphabet's — sorted alphabetically, the municipality would come first and the selectors would invert. The third case resolves the expected levels from the database instead of hardcoding them, so loading a further level fails here rather than silently in the form.
The location work shipped with no test behind it. Two files, both pure enough to run without mounting the form: `mappers` — that only the innermost level answered is sent, since the ancestors are implied by the hierarchy and sending them too would let the two disagree; and that a chain shorter than the five selectors pads rather than shifts. `useTerritorySelectors` — that a level with no rows is never rendered, that a level loaded later appears without a code change, and that a selector whose branch ends stays locked instead of offering an empty list. The Distrito Nacional is the case that exercises the last one: it is itself the municipal level, so it has no municipios.
mrivas00
force-pushed
the
docs/mati/rd-branch-freeze
branch
from
August 26, 2026 20:21
719052a to
c42391b
Compare
mrivas00
force-pushed
the
feat/mati/rd-organization-form
branch
from
August 26, 2026 20:21
dceb2a7 to
68fd5ef
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The Dominican registry asks an organization for two things the platform had no
field for: a secondary economic activity and an administrative location.
PR 3 added the columns; this fills them in and makes the delete-warning dialog
account for the new reference.
Seventh of the RD stack, based on
docs/mati/rd-branch-freeze.What changes
Secondary economic activity. One flat search over the whole catalog,
labelled
Sector — Actividad, not a second sector-plus-activity pair — a hotelwith an agricultural operation has to be able to declare a secondary activity in
another sector.
It does depend on the primary, but only by exclusion: the selector waits until
the primary is chosen — declaring a secondary activity and no primary one is
incoherent, and the API persists
secondarySubsectorIdwithout a cross-check —and the primary is filtered out of its options. Setting the primary to whatever
the secondary already holds clears the secondary, which is otherwise a way to
submit the same activity twice while the field renders empty.
Territorial location.
GET /api/territoriesreturns the children of a node,so each dependent selector loads only the siblings under the node the previous
one produced. The whole tree is never served: the official hierarchy reaches the
sector level, which is roughly thirteen thousand rows. Only the innermost answer
the registrant reaches is stored; the ancestors are derived by walking four
nested parents, which is the whole chain at five levels deep.
A second endpoint,
GET /api/territories/levels, says which levels the catalogactually holds rows for. The form renders one selector per level returned, so the
levels still waiting on a source are absent rather than dead, and appear on their
own the day they load — no hardcoded list to keep in sync. Every rendered
selector stays on screen while it is locked, labelled with the answer that
unlocks it, the way the economic activity behaves while its sector is
unanswered.
Dirección / RegiónbecomesDirección física, independent of the hierarchy,and joins the selectors in a location section of its own, laid out as the last
cell of the same two-column grid so an odd number of levels leaves no hole.
The representative's identity document.
ID representantebecomesDocumento de identidad del representante, with the note saying which oneapplies: cédula for Dominican nationals, passport for foreign nationals without
one. The kind of document is not stored — the number identifies it, a cédula
carries the country's fixed format and a passport does not, and nothing reports
on the distinction. Observation 3 asks to "considerar un selector"; a selector
whose value is discarded is worse than none, so this ships the half the
observation states outright and leaves the column for the day something reads it.
Delete warnings count secondary references. The count the dialog shows was
understating the impact: an organization whose only link to a row is its
secondary activity was invisible, and deleting a sector cascades to its
activities all the same.
_countcannot express the union of two columns, so thecount moves to a
groupByover both — which is also what makes an organizationthat declares one activity as primary and secondary count once instead of
twice.
The activity-unit list follows the primary activity.
useOrganizationDatabuilds the intensity-denominator filter from sector and subsector, and the endpoint reads both, but the dialog never passed the subsector — so half the filter was alwaysundefinedwhile the form went on clearing the field every time the primary activity changed, as though it depended on it. The filter is additive (the generic denominators are always included), so passing it can only widen the list. Today it changes nothing: all 19 seeded rows are either generic or per-sector, none per-subsector. It starts mattering the day MMARN adds a denominator for one activity — a data change that would otherwise have failed silently.Tests
GET /api/territories/levelsshipped without an integration test; it has one now. The cases that matter are that the two levels waiting on the IDE-RD layers stay out of the response, and that the order is the hierarchy's rather than the alphabet's — sorted alphabetically the municipality would come first and the selectors would invert.Two web test files cover the rest: that only the innermost territorial level answered is sent (the ancestors are implied by the hierarchy, and sending them too would let the two disagree), and that a selector whose branch ends stays locked instead of offering an empty list. The Distrito Nacional exercises the last one — it is itself the municipal level, so it has no municipios.
Judgement calls
The territorial catalog stops where the sources do. Article 7 of the Ley
Orgánica de Regiones Únicas de Planificación (núm. 345-22) enumerates the ten
planning regions, the thirty-two provinces and the 157 municipios in one place,
and the ONE's División Territorial 2021 reports the same counts. All three levels
ship from it. The municipal district and the sector/paraje do not: the IDE-RD
layers that carry them are identified but not obtained, and a partial list would
let a registrant find their province and not their municipality, which is worse
than offering neither.
territory_levelkeeps all five values even so. The observation asks for fivelevels, so the model represents five; only the data is partial, and the form asks
the catalog which levels exist rather than assuming.
docs/development/rd-territories-sources.mdrecords the source, what corroborates it, and what is still unverified — the law
is a snapshot of August 2022, and municipios are created and promoted by later
laws.
The catalog also corrected five rows against the law. The first draft followed
Decreto 710-04, which Ley 345-22 replaced. Azua sat under Valdesia rather than El
Valle — which takes Valdesia from four provinces to three and El Valle from two
to three — and four rows carried wording the ministry does not use:
Baoruco,Monte Cristi,Higuamo,Ozama o Metropolitana. The migration in PR 3 carriesthe same correction.
seedTerritoriesis the second path, not the first. PR 3's migration loadsthe catalog, because
seed.tsskips entirely once a country exists and the RDenvironment is already populated. This seeder runs on a database created without
migrations and returns early when the table already holds rows — before reading
the file, which is why the
testingfixture is gone and the API tests assertagainst the real Dominican hierarchy instead. Its assertions are structural
wherever they can be, so a province MMARN renames does not break the suite.
The new mutation fields are required-nullable, like every other field on
OrganizationMutationDataSchema, which is.strict(). That is why 19 existingtest payloads gain two keys: a client has to be explicit about them.
Also corrects three labels the terminology pass missed —
Actividad principaland
Direcciónin the profile view, and the whole label set of the/forms/organizationsendpoint, which mirrors the form.The submission review dialog shows the new fields too. The API already
returned them —
getOrgSummaryDetailsincludes the secondary activity and theterritory with its ancestors — but the dialog never rendered either, so an
administrator approving an inscription could not see the location the registrant
declared. It also still carried
Actividad principalandDirección, two labelsthe terminology pass renamed everywhere else. Only the levels the organization
answered are rendered, and the chain-building the profile view did inline becomes
getTerritoryChain, now that two screens present the same location.And two layout rows the reordering broke. Pairing a field carrying
flex-1with one that does not leaves no free space to distribute, so the
flex-1fieldcollapses:
Cantidad de trabajadoreswas squeezing the activity unit to atruncated sliver, and
Correo, having lost its class and its spacer, spanned thewhole dialog. The activity unit now sits beside the secondary activity, where the
related questions belong.
Verification
pnpm format,pnpm lint,pnpm type-checkandpnpm test:web(723 tests) rungreen.
The API tests in this PR are unrun — no Postgres or Docker in the authoring
environment. That covers the territories-endpoint tests — including the three
cases the municipal level introduces: a province returning its municipios, a
municipio as the innermost leaf, and the Distrito Nacional, which is itself the
municipal level and so returns none — plus the two profile-field round trips, the
four reference-count tests, and every existing organization payload the schema
change touches. The seed is also unrun:
seedTerritorieswrites depth by depth,now three rounds rather than two, and nothing has exercised it against a
database.
No migration. No check runs on this PR:
ci.ymltriggers onpull_request: branches: [main].