Skip to content

feat(publishers)!: drop the Member.type column - #378

Merged
mindsers merged 2 commits into
mainfrom
feat/drop-member-type
Aug 31, 2026
Merged

mindsers merged 2 commits into
mainfrom
feat/drop-member-type

Conversation

@mindsers

Copy link
Copy Markdown
Contributor

Do not merge yet. Two prerequisites below.

Final step of removing Member.type. It cached a member's standing pioneer status; PioneerEnrolment is the source of truth for the same fact and has been since the plan/actual split — the schema comment on that model already said so. The previous release moved every reader onto the stints and backfilled anyone holding a type without one, so this removes what nothing reads any more.

The member_pioneer_requires_baptism CHECK is dropped first; Postgres refuses to drop a column a constraint still references.

Two behaviours were translated, not deleted

Turning off the permanent-auxiliary profile. It used to flip every auxiliary member's cached type. It now closes their ongoing auxiliary stints at the current month, via a new endOngoingEnrolmentsOfType — the same intent against the source of truth, keeping the history of what they actually did. The old version was already incoherent: it flipped the cache and left the stints open, so the two disagreed immediately, and after the previous PR the recompute would simply have undone the demotion.

Filtering the publisher list by type. Now filters on stints — an ongoing stint of that type, or for Normal the absence of one. A single-month auxiliary is closed, so it still reads as Normal, exactly as the column did.

One deliberate product change

The create form no longer offers a pioneer type. A pioneer appointment is an enrolment with a period, made from the pioneer section of the edit page — which is where the edit form already sent people (hideTypeSelect). A member is created Normal and appointed afterwards. This also closes the hole that produced pioneer-typed members with no stint in the first place.

Also

The S-21 export and the group roster derive the status from the stints (the group roster turned out to select the column and never render it). The archive export stops emitting the field; the importer ignores it on older archives. The activity-derived backfill loses its memberType fallback, so contiguous auxiliary months backfill as single-month stints — snapshots cannot distinguish a monthly auxiliary from a permanent one, and single-month is the safe guess because it grants no standing status.

Three readers the compiler could not see

A where clause inside an OR, a spread-built filter, and a spread select all pass typecheck and fail at runtime. They were found by applying the migration locally and running the whole suite against a database without the column — the only check that catches this class, and worth repeating for any future column removal.

Verified

3438 unit, 412 integration, and all eight architecture gates — against a database where the column is actually gone, not just against the schema. TDD for the new service function (endOngoingEnrolmentsOfType tests written first) and for the schema change; the publisher-filter tests were written after the fact, so I proved them load-bearing by restoring the old filter and watching them fail.

Merge order

  1. unitae-platform feat: redesign display board UX #68 — must be deployed before this merges. Platform CD applies this repo's migrations before rolling its own image.
  2. unitae-platform fix: improve unsaved changes guard across all forms #69 — the schema copy, alongside or after this.
  3. this PR.

Final step: PioneerEnrolment has been the source of truth for a member's standing
pioneer status since the plan/actual split, and the previous release moved every
reader onto the stints and backfilled anyone holding a type without one. This
removes what nothing reads any more, plus the CHECK constraint that referenced it
(dropped first, or the column drop fails on the dependency).

Two behaviours had to be translated rather than deleted, because the column was
carrying them:

Turning off the permanent-auxiliary profile used to flip every auxiliary member's
cached type. It now closes their ongoing auxiliary stints at the current month via
endOngoingEnrolmentsOfType — the same intent against the source of truth, and it
keeps the history of what they actually did. The old version was already incoherent:
it flipped the cache and left the stints open, so the two disagreed immediately.

Filtering the publisher list by type now filters on stints — an ongoing stint of
that type, or for Normal the absence of one. A single-month auxiliary is closed, so
it still reads as Normal, exactly as the column did.

The create form no longer offers a pioneer type. A pioneer appointment is an
enrolment with a period, made from the pioneer section of the edit page, which is
where the edit form already sent people. The form schemas drop the field, so a
member is created Normal and appointed afterwards.

Also: the S-21 export and the group roster derive the status from the stints; the
archive export stops emitting the field and the importer ignores it on older
archives; the activity-derived backfill loses its `memberType` fallback, so
contiguous auxiliary months now backfill as single-month stints — the snapshots
cannot distinguish a monthly auxiliary from a permanent one, and single-month is the
safe guess because it grants no standing status.

Three readers were invisible to the compiler and only failed at runtime: a `where`
clause inside an `OR`, a spread-built filter, and a spread `select`. They were found
by applying the migration locally and running the whole suite against a database
without the column, which is the only check that catches this class.
…ry close

Two real defects from review, both in the path that runs when a congregation turns
off the permanent-auxiliary profile.

A future-dated stint made the whole settings save fail. The create form offers start
years from -2 to +2, so an auxiliary appointment can be dated ahead. Closing it at
the current month is an end before its start, which closeEnrolment rejects — and
because the close runs congregation-wide, that one stint threw and took every other
member's close with it. Stints that have not started are now left open and counted
in the return value rather than attempted.

The close month came from the server's clock. Closing picks a MONTH, so a server an
hour either side of midnight on the 1st dated it to the wrong one. It now takes the
congregation's clock via zonedNow, matching how pioneer-goals and the dashboard
already resolve "now" for congregation-scoped work.

Also from review:
- An integration test for the standing-type filter. The unit test asserts the `where`
  shape, which cannot tell whether Prisma builds SQL that selects the right people —
  and this branch already produced three column reads that typechecked and failed
  only at runtime. It pins that a closed single-month auxiliary reads as Normal.
- A test that the bulk close spans several members, which the single-member cases
  never exercised.
- `getPublishersWithGroup`'s filter option is now `standingType`, matching the rename
  the role predicate already took; it selects on stints, not on a member field.
- The S-21 export selects only the stint fields it reads, via an ENROLMENT_PERIOD_SELECT
  kept beside EnrolmentPeriod so the two cannot drift. It was fetching whole rows for
  every publisher in a congregation-wide zip to read three fields.
- Eight test names still described setting `Member.type`; they assert standing status.
@mindsers
mindsers merged commit 436d14c into main Aug 31, 2026
7 checks passed
@mindsers
mindsers deleted the feat/drop-member-type branch August 31, 2026 19:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant