Skip to content

Reconcile facilitator affiliations against attendance (admin bulk action) - #2195

Open
maebeale wants to merge 50 commits into
mainfrom
maebeale/facilitator-affiliation-reconciliation
Open

Reconcile facilitator affiliations against attendance (admin bulk action)#2195
maebeale wants to merge 50 commits into
mainfrom
maebeale/facilitator-affiliation-reconciliation

Conversation

@maebeale

@maebeale maebeale commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

🤖 suggested review level: 5 Inspect 🔬 changes what reconciliation writes to affiliation history, which the grant figures read

Brings facilitator affiliations in line with who actually attended a training, as an admin bulk action with a preview-and-confirm step.

What reconciliation does, by attendance status

Status Outcome
attended Keep active
no_show, cancelled, incomplete_attendance Delete the row this training minted; end any older row the day before
transferred_out Move the open row to the destination event and re-point its provenance
registered Report only — an unfilled roster isn't an outcome

transferred_in isn't a status (it's an FK), so those reconcile on whichever status they hold.

Why deleting, not deactivating

A row minted by this training's registration records an assumption: that the person would become a facilitator on the training date. If they didn't attend there's no period to preserve, and a zero-length row is a worse way of saying it never happened.

Anything older — hand-entered, or from an earlier training — is only ever end-dated. Deleting those would erase years of real history and move the organization's program status at every training in between. That distinction is the core of this PR.

Why the dates matter

Program status (New / Ongoing / Reinstated) is anchored to each training's date and feeds the annual grant report. A reconciliation that rewrites an affiliation's dates rewrites those figures retroactively — an organization that read Ongoing at its 2026 training could start reading Reinstated afterwards. The end date is the day before the training, matching ApplyScenarioEndDating, so a row ending as another starts can't double-count the person.

Also here

  • Active / Inactive tabs on the person and organization affiliation editors — server-rendered, no JavaScript (:has() on two detached radios)
  • inactive as a deliberate override — an explicitly supplied flag survives a later date edit, and an end date of today or earlier ticks it for you
  • Admin → Data health — consistency checks spanning every record, two of them repairable
  • ADR-0003 — the two relationships one table carries, historical vs current reads, and the rules above

Notes for review

  • docs/adr/0003-affiliations-as-the-record-of-two-relationships.md is the fastest way in; D6/D6a–D6d are the reconciliation rules
  • Organizations with a stale "Active" status column and no active affiliation will start rendering as unpublished — that's ADR-0001 D3a's drift, surfaced rather than introduced

Copilot AI lite review requested due to automatic review settings August 14, 2026 04:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@maebeale
maebeale force-pushed the maebeale/facilitator-affiliation-reconciliation branch from 68913c9 to e765a96 Compare August 14, 2026 04:58
Copilot AI review requested due to automatic review settings August 14, 2026 04:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@maebeale maebeale changed the title Reconcile facilitator affiliations against attendance Reconcile facilitator affiliations against attendance (admin bulk action) Aug 14, 2026
@maebeale
maebeale marked this pull request as ready for review August 14, 2026 05:11
Copilot AI review requested due to automatic review settings August 14, 2026 05:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.


def facilitator_start_date
(@training_date || Date.current).to_date.beginning_of_month
(@training_date || Date.current).to_date

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Claude: Intentional shift toward accurate dates: this starts the facilitator affiliation on the actual training date rather than the first of its month (changing code that just landed in #2194). Reconciliation works either way; flagging since it edits a freshly-merged file.

Copilot AI review requested due to automatic review settings August 14, 2026 12:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 14, 2026 12:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 14, 2026 13:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 14, 2026 13:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 14, 2026 13:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 14, 2026 13:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 14, 2026 14:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@maebeale maebeale changed the title Reconcile facilitator affiliations against attendance (admin bulk action) MAYBE: Reconcile facilitator affiliations against attendance (admin bulk action) Aug 14, 2026
@maebeale maebeale changed the title MAYBE: Reconcile facilitator affiliations against attendance (admin bulk action) EXTRA: Reconcile facilitator affiliations against attendance (admin bulk action) Aug 14, 2026
Copilot AI review requested due to automatic review settings August 14, 2026 17:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 14, 2026 23:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

maebeale and others added 29 commits August 25, 2026 11:29
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Same-daying an affiliation on or after its start date left it active: the
callback always recomputed `inactive` from the dates, and a row ending today
still reads as active. Admins also had no way to set it — the column was
permitted everywhere but had no field, and ticking it alongside a date edit was
silently overwritten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The classification lived in ReconcileEvent while ReconcileFacilitatorAffiliation
held a second, owned-only copy that nothing but its own spec reached — two
implementations of the same rules, already disagreeing on hand-entered rows.
ReconcilePerson is now the only place a decision is made; ReconcileEvent
iterates it and keeps the keys, grouping and timestamp. Owned-vs-all becomes an
argument, so the per-person reconciler is callable on its own (e.g. from an
attendance change) without re-deriving anything.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The badge's Turbo submit answered with a stream that swapped only the status
chip, so the row kept offering its pre-toggle action and the new return_to
redirect never ran. Pages that pass a return_to now opt out of Turbo and get the
full re-render; the registrants and onboarding pages keep the inline swap.

Also lists the bulk action on the Features & tips seed and drops the "uncheck"
wording left over from before the row controls became radios.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Main moved per-affiliation editing to the gear editor, so that — not the dense
inline row — is where the flag belongs. Trims the comments added across this
branch down to the ones carrying a why.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The person History card and activity timeline had nothing multi-year to render,
so affiliation edits, trainings, memberships and comments couldn't be seen
interleaved. Two gaps kept the seeded rows invisible: affiliation comments were
missing from PersonCommentAggregator (Affiliation became commentable in #2235
without being added), and payment lifecycle events record the STI subclass
("CashPayment"), which the person's Payment filter never matched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Inactive checkbox only held until the next time anyone touched a date. The
guard tested `inactive_changed?`, which is false when a form re-submits the value
the record already holds, so the date rule ran and derived the flag away — an
unrelated start-date edit silently reactivated a row an admin had ended.

`inactive_supplied` records that a caller set the value on purpose. The standalone
editor always posts the checkbox, so the controller sets it from the params; the
nested rows set it when their end date changes. It is a cast writer because forms
send "0", which is truthy in Ruby and would otherwise suppress the date rule on
every nested row.

An end date of today or earlier now ticks the box for you. The date rule compares
strictly, so today alone still reads as active — the flag is what carries it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deactivating same-dayed every row, including ones minted years earlier by a
different training. That erased the whole period a person actually facilitated,
and the anchored program status moved with it: an organization that read Ongoing
at its 2026 training read Reinstated afterwards, changing figures that back grant
applications.

Only the row this training minted collapses to its start date — it recorded an
assumption that never came true, and a strict `<` already excludes it from its own
anchor. Anything older ends on this training's date instead, so the years before
it survive.

Reactivation had the mirror problem: clearing an end date swallowed the gap, so
"Art program since" collapsed `Jan 2023 – Jan 2024, Aug 2026` into `Jan 2023`. A
return is now a new row, which is what CreateFromRegistration has always done, so
`:reactivate` is gone entirely.

Why a row changed is recorded as a comment on the affiliation rather than a new
column — the edit page already surfaces them, and the comment topic is enough to
stop labelling an admin-ended row "didn't attend".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…atus column

`Organization.active` counted a stored "Active" status as enough on its own, and
`#published?` short-circuited on it before ever looking at affiliations — so an
org whose column had drifted read as active with nobody facilitating there. ADR-0001
D3 says the column plays no part; these two were the exceptions.

Also replaces nine open-coded copies of `!inactive? && (end_date.nil? || end_date
>= today)` with `active?`. The rule now lives in one place, which matters more now
that the flag can disagree with the dates.

Expect orgs with a stale "Active" column and no active affiliation to start
rendering as unpublished. That is the drift ADR-0001 D3a warns about, surfaced
rather than introduced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`active?` and `active_on` sat two characters apart while answering different
questions with different inputs — one reads the dates and the inactive flag to say
what is true now, the other reads dates alone to say what was true on a date. The
new name says which input it uses.

ADR-0002 writes down what ADR-0001 left implicit: the two relationships the one
table carries, that `inactive` is now an override rather than a cache, what
`event_registration_id` does and does not mean, and the two rules above about not
erasing history.

The arithmetic behind the grant figures is covered directly rather than inferred
from single-affiliation cases — several people at one anchor, Jan 1 vs Dec 31 in
both directions, a full new → ongoing → reinstated → ongoing walk, and that
reconciling a no-show leaves an anchored verdict where it was.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Long affiliation lists mix people who facilitate now with rows that ended years
ago. The server already knows which is which, so it renders both groups and two
radios drive the visibility through :has() — no JavaScript, and the person and
organization forms share one partial instead of two copies.

A row you end while editing stays exactly where it is and just restyles; its
bucket only moves once you save. That is why this is not the registrants page's
server-round-trip filter: switching tabs must not discard unsaved edits.

Two Tailwind traps shape the markup. Radio ids cannot contain underscores —
Tailwind reads `_` as a space inside an arbitrary value, so the selector matches
nothing. And the group is named, because `group-hover:` matches any `.group`
ancestor and an unnamed one made hovering pop every row's comment tooltip at once.

The standalone editor now uses the same live styling, its comment icon opens the
comments it is previewing, and a back link to an ended row lands on the section
rather than a row hidden on the other tab.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An end date of "yesterday" computed in UTC is still today in the viewer's zone for
part of each day, and ApplicationController sets the zone per user — so the row
read as active and the redirect landed on it instead of the affiliations section.
A month back removes the ambiguity. Same failure mode #2264 just fixed on the
recipients program-status spec.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
inactive-toggle decides "ended" from the browser's local date, while Ruby's
Date.current follows the Rails zone — for part of each day they are different
dates, and the end-date-of-today examples failed on the difference rather than on
the behaviour. Asking the browser for its own today tests what the controller
actually compares against.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#2292 centralized the muted gray for back-nav links while this branch was in
flight, so the two reconcile screens were the only ones still hardcoding it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FacilitatorProgramStatus#active_on_anchor is private to a file upstream edits
often — #2295 rewrote comments in it during this branch's life — so renaming it
here bought a conflict on every rebase and no clarity at any call site. That file
is byte-identical to main again.

The scope keeps the name: Affiliation.active_by_date_on is what a new caller
sees, and it is the one that has to be distinguishable from #active?.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Some inconsistencies only show up across the whole table. The reconcile page finds
facilitator affiliations minted by a non-training registration one event at a time,
and the org edit form warns about legacy-status drift one organization at a time —
neither answers "how many are there, everywhere".

Three checks to start:

  - facilitator affiliations from non-training events, which count toward program
    status without anyone having trained (deletes them, via destroy so the
    organization's status and dates stay in step)
  - affiliations whose minting registration belongs to a different organization,
    which breaks ADR-0002 D2a's invariant and makes reconciliation treat the row as
    auto-created for an org it never belonged to (unlinks them, the conservative
    direction: the row becomes hand-entered, which reconciliation spares)
  - legacy organization-status drift, which reports only. The stored vocabulary has
    no value meaning "never active", and the affiliation callbacks only ever write
    Active/Inactive, so any automatic rewrite would drift straight back.

Report-only is the base class default rather than an exception, because a wrong row
is not always one we know how to put right. The repair route resolves its param
against the registered checks and refuses anything else, so a report-only check
can't be coaxed into running one.

Also qualifies `affiliations.title` in the `.facilitators` scope. It broke as soon
as the scope was joined to `events`, which has a title of its own — the same
ambiguity `.active` already guards against for `end_date`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A row minted by the registration for this training records an assumption: that the
person would become a facilitator on the training date. When they don't attend,
there is no period to preserve — a zero-length row that reads as "ended the day it
started" is just a worse way of saying it never happened.

Older rows are unchanged: hand-entered ones, and ones from an earlier training,
still end on this training's date. Those record facilitation that really happened,
and deleting them would erase years of history and move the anchored program status
at every training in between.

This is what lets reconciliation stop leaning on the inactive flag. A same-dayed row
could land on today and still read as active by dates alone, which is the case the
flag existed for here. A deleted row has no such problem, and an older row ends on a
training date that has already passed, so the date rule derives the flag by itself.

The cost is that a deleted row takes its comments with it, so the reason D6b records
survives only for ended rows. The deletion is still on the record as a
destroy.affiliation event with the full attribute snapshot, and there is a spec for
that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Main landed its own ADR-0002 (org linking flows and agreement scenarios) while
this branch was in flight, so both files claimed the number — and both define a
D4, which made every "ADR-0002 D4" in a comment ambiguous. This branch's ADR moves
to 0003 and only the references that point at its decisions move with it; the ones
naming main's scenario work stay put.

Main also grew an AffiliationDecorator#period_label that renders an affiliation's
span, which is what this branch's #date_range was doing. Dropped the duplicate and
switched the reconcile screens to period_label — month precision rather than day,
matching ADR-0001 D2 and every other affiliation period in the app.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Main grew its own live tinting on this editor while the branch was in flight, and
taking its version of the field block dropped two things from this branch: the
checkbox's inactiveCheckbox target, and the end-date field's endDateChanged action.
Without them the box neither drove the styling nor ticked itself for a past end
date. The system specs caught both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two statuses were being treated as "didn't attend" when they aren't.

`registered` after the event is a gap in the record, not an outcome —
#attendance_recorded? excludes it for exactly that reason. Deleting a facilitator
affiliation because nobody filled the roster in is acting on missing data, and the
deletion doesn't come back. Those rows are now left alone and listed under
"Attendance never recorded — set an outcome first", which also nudges someone to go
fix the roster. Cancelled and transferred-out stay as they were: those are
decisions somebody made.

`incomplete_attendance` still ends the affiliation, but the page now shows the
sign-in sheet day by day underneath the person. "Incomplete" is a judgement someone
recorded and the logged times are the evidence — an admin about to delete a
facilitator affiliation should be able to see which days were missed without
leaving the page.

The status is looked up rather than read off the passed registration, because
callers that only want a plan don't have to pass one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The row read as a sentence — "stored Active, affiliations say Never active" —
which buries the only thing that matters: the two answers disagree. Side by side
as chips, in labelled columns, the disagreement is the first thing you see.

Both chips come from OrganizationDecorator.status_classes_for_bucket, the same
DomainTheme lookup the org profile and index use, so a bucket means the same colour
here as everywhere else and the two columns are actually comparable.

Checks whose rows are worth comparing declare `columns` and a `row_partial`; the
page still falls back to the one-line #describe for the rest, so nothing else had
to change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Transferring to another event isn't failing to complete one — they're still going
to train, somewhere else. So the affiliation follows them: its start date becomes
the destination event's and its event_registration_id re-points at the destination
registration.

Re-pointing the provenance is load-bearing, not tidying. The FK is the
auto-vs-manual gate (ADR-0003 D2a), so a row dated to the destination event but
still pointing at the source would not be recognised as the row that training
minted when the destination is reconciled — it would be treated as an older row and
end-dated.

Only the row with no end date moves; an already-ended row records a finished
stretch and stays put, with a fresh affiliation created at the destination instead.
A destination that hasn't been recorded yet, or one linked to a different
organization, is reported rather than guessed at. Chained transfers need no
traversal: A→B→C collapses when the second transfer is made, so the source already
points at the final destination.

Built as a fourth action on ReconcilePerson rather than a LinkSubmittedOrganization
scenario — those describe what kind of linking is happening and run at link time,
while a transfer is an attendance outcome found at reconcile time with nothing
being linked.

Also moves the end date for an older row to the day BEFORE the training, matching
ApplyScenarioEndDating: a row ending the same day another starts counts on both and
doubles the person in any report totalling a date.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ApplyScenarioEndDating and reconciliation both end an affiliation the day before
the date that supersedes it, for the same reason: a row ending as another starts
counts on both. They were documented independently, so a reader who found one
would reasonably assume it was local to that flow and pick a different rule for
the next caller. Each ADR now names the other.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The argument for it was that an org whose only facilitator is ended keeps reading
Ongoing at that training. But the row is ended precisely because there is no record
the person ever completed a training for that organization — so ending it ON the
training date leaves the org's status at that training resting on the affiliation we
just decided wasn't valid. Ending the day before is what the decision means.

Reinstated at that anchor isn't a cost paid for consistency; with the row gone there
is no basis for Ongoing, and none of the three labels fits "we removed the basis for
their status" cleanly.

No behaviour change — the code already ended everything the day before. This is the
ADR catching up, plus three UI strings that still said "this training's date".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two things made it fragile. It hardcoded `wait: 2`, undercutting the suite's own
5s default — which exists precisely because "2s is too tight for selenium + Turbo
on loaded CI runners". And hover is a one-shot mouse move: unlike an assertion it
never retries, so if the row shifts under the cursor while Stimulus settles the
styling, the move lands on nothing and waiting longer can't recover it.

Now scrolls the icon under the cursor, re-hovers if the first attempt missed, and
uses the default wait. The assertion itself is unchanged — it still fails if
hovering one row's icon reveals another row's tooltip, which is the collision it
was written for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Headless Chrome on CI never applies the CSS :hover from Selenium's synthetic
mouse move, so the assertion could not pass there no matter how many times it
retried. The bug being guarded is structural anyway — an unnamed `.group` on an
ancestor makes `group-hover:` fire for every row at once — so assert that
invariant against the rendered DOM instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…liation

Dropping :reactivate entirely went too far. A no-show corrected to attended left
the person with two facilitator affiliations for one organization, implying an
engagement that ended and restarted when nothing had lapsed — the row was ended by
our own inference from attendance that has since changed.

A reopen now applies only where the ending is provably ours: the end date is
exactly what reconciling this training writes AND the row carries a reconciliation
comment. An admin's ending on the same date, or an earlier training's, still reads
as a real lapse and the return is still a new row. Where a reopen applies, no
create is proposed, so the two can't both fire. The admin can overrule with
"Create a new one instead".

ADR-0003 D6a rewritten accordingly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Will be reactivated" / "Will be created" / "Will be deleted" described an outcome
the admin hasn't chosen yet; the buttons are the choice, so they read as verbs.

Two body-text assertions keyed off those labels and would now match the wrong row
("Delete" is also a label on a deactivate row) — they assert on the row's own radio
ids instead, which don't move when copy does.

Also records in ADR-0003 D6a that a reopen is tied to the training whose attendance
changed: attending a later training while an earlier ending stands is a new
engagement, not a correction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A registrant still marked Registered after the event is a gap in the record, and
the page could only say so — the fix lived on another screen. The day ticks now sit
beside the status chip and reuse the onboarding cell, so ticking a day rolls the
status forward and repaints both the tick and the chip in place.

They render once per registration: someone who linked two organizations gets two
rows in that bucket, and the turbo target ids have to stay unique.

Also: buttons say "Create new FA" with the abbreviation on hover, the header note
no longer overstates what the buttons do before a preview, and an end date nobody
reconciled reads as what it is rather than as a reconciliation outcome.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@maebeale
maebeale force-pushed the maebeale/facilitator-affiliation-reconciliation branch from de2051a to 05d4e3a Compare August 25, 2026 15:34
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.

3 participants