Skip to content

Organization deduper (festi-inspired, on the existing dedup framework) - #2374

Open
maebeale wants to merge 9 commits into
mainfrom
maebeale/organization-deduper
Open

Organization deduper (festi-inspired, on the existing dedup framework)#2374
maebeale wants to merge 9 commits into
mainfrom
maebeale/organization-deduper

Conversation

@maebeale

@maebeale maebeale commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

🤖 suggested review level: 5 Inspect 🔬 substantive merge logic that reassigns every FK reference across ~20 tables, repoints analytics/audit, and deletes records

Admin-only organization deduper, built on the existing dedup framework (ModelDeduper + Dedupable + shared dedupes/ views), extended for FK-based models. Built festi-first, then an awbw design pass.

Engine (ModelDeduper)

  • Reassigns every association that references the model by FK — plain-FK children, polymorphic :as children, and tagging joins.
  • Union of two sources: declared has_many/:as plus a schema-driven scan of DB foreign keys, so FK children with no inverse has_many (payments, stories, monthly_reports, …) are reassigned rather than orphaned.
  • Collisions resolved from DB unique indexes — never guessed. Reproduces Category/Sector behavior exactly; avoids wrongly deduping records that merely share an unrelated polymorphic (reports.owner).
  • Analytics + audit (ahoy_events, versions) are repointed to the kept record so history follows the survivor instead of orphaning.

Coverage safeguard

  • A data-driven scan (unhandled_references) checks the actual record for any table that still references it but that the merge wouldn't reassign — independent of dependent: options.
  • If found, the preview shows a top-of-page banner, a warning by the button, and the Merge button is disabled; dedupe_perform also refuses server-side. So a future association added to Organization can't silently break referential integrity.
  • Attached files (logo) that are deleted, not moved are surfaced on the preview so the loss is visible.
  • Billing (Pay gem) links are deferred — they neither block nor move — pending a product decision in Decide how organization merge should handle billing (Pay gem) links #2378.

Detection (OrganizationServices::DuplicateFinder)

  • Clusters by normalized name, name-minus-legal-suffix, FileMaker code, and shared address.
  • FileMaker codes are a trimmed, comma-separated set — matched by set-intersection; on merge the kept org keeps the union of both records' codes.

UI (awbw)

  • DomainTheme colors, sentence-case copy, btn classes, eyebrow nav, blue form-focus.
  • Index: annotated candidate groups (reason chips, suggested-keeper badge, per-pair preview links).
  • Preview: side-by-side with curated editable fields, "Will be dropped"/"Kept on merge" badges, and a reassignment summary that lists a few truncated record names per association (not just counts).
  • Entry points: a "Dedupe" text link on each index (organizations/categories/sectors) and cards under Deprecated data on admin home.

Screenshots

In .context/screenshots/: dedupe_index.png, dedupe_preview_final.png, dedupe_blocked.png, dedupe_filemaker.png, admin_deprecated_dedupe_card.png.

Follow-up

Tests

FK reassignment (restrict-dependent, natural-key collisions, FK-only children); analytics/audit repoint; lost-file reporting; coverage-safeguard block; finder clustering incl. multi-code; FileMaker union; org request flow. All dedupe/org/finder/model/admin specs green.

maebeale and others added 5 commits August 25, 2026 09:56
…red)

Extend ModelDeduper to reassign every FK reference to a model (not just
polymorphic tagging joins), unioning declared has_many/:as associations with a
schema-driven scan of DB foreign keys so FK children without an inverse has_many
(payments, stories, monthly_reports, …) are reassigned rather than orphaned or
blocked by their FK constraint. Collisions are resolved from DB unique indexes.

Add OrganizationServices::DuplicateFinder (name, legal-suffix, FileMaker-code,
and address clustering, with FileMaker treated as source of truth) and wire
OrganizationsController into the Dedupable concern with a custom candidate
finder. The index now renders annotated candidate groups with per-pair preview
links.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the generic festi-style preview with awbw conventions: DomainTheme
colors, sentence-case copy, btn classes, eyebrow nav, and the standard blue
form-focus. The preview now shows a meaningful reassignment summary (counts of
each association that moves) instead of a single polymorphic join's items, and
orgs edit only a curated set of fields rather than every column. Index groups
gain reason chips, a suggested-keeper badge, and per-pair preview links. Add a
Dedupe entry point on the Organizations page and a Features & tips entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FileMaker is the source of truth and a record can map to more than one
FileMaker record, so the deduper now reads filemaker_code as a trimmed,
comma-separated list. Candidate detection matches on set intersection (FM-27 no
longer matches FM-273), and merging keeps the union of both records' codes so no
FileMaker linkage is lost. The preview marks a union field being combined with a
'Kept on merge' badge rather than 'Will be dropped'. Adds a generic union_columns
+ merge_keeper hook to the Dedupable framework.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@maebeale
maebeale marked this pull request as ready for review August 25, 2026 14:58
maebeale and others added 2 commits August 25, 2026 11:01
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
maebeale and others added 2 commits August 25, 2026 11:26
…refs

- Reassignment summary now lists a few truncated record names per association,
  not just the rollup count.
- Merge repoints ahoy_events + versions to the kept record so analytics and audit
  history survive instead of orphaning.
- Preview surfaces attached files (logo) that are deleted with the duplicate, and
  adds space above the action buttons.
- Coverage safeguard: a data-driven scan blocks the merge (banner + disabled
  button + server guard) if anything references the record that the deduper won't
  reassign, so a future association can't silently break referential integrity.
  Billing (Pay gem) links are deferred to #2378.

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

- Reassignment summary now lists a few truncated record names per association,
  not just the rollup count.
- Merge repoints ahoy_events + versions to the kept record so analytics and audit
  history survive instead of orphaning.
- Preview surfaces attached files (logo) that are deleted with the duplicate, and
  adds space above the action buttons.
- Coverage safeguard: a data-driven scan blocks the merge (banner + disabled
  button + server guard) if anything references the record that the deduper won't
  reassign, so a future association can't silently break referential integrity.
  Billing (Pay gem) links are deferred to #2378.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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