Multiple reports attached to communication #149

Closed
opened 2026-07-20 14:06:46 +00:00 by eliribble · 20 comments
Owner

Good morning!

Well I'm sure your morning was better before you got this email. So I'm still getting doubles on the reports but this one is a weird one. It has multiple reports attached all from different times and people. Report ID:

#3AMRLP5XSC5G

The only commonality is that they have "no address provided".

Alysia Davis
Vector Control Operations Analyst
Delta Mosquito and Vector Control District

Good morning! Well I'm sure your morning was better before you got this email. So I'm still getting doubles on the reports but this one is a weird one. It has multiple reports attached all from different times and people. Report ID: #3AMRLP5XSC5G The only commonality is that they have "no address provided". -- Alysia Davis Vector Control Operations Analyst Delta Mosquito and Vector Control District
Author
Owner

Communication ID #546 which shows the following attached reports:

  • 3AMRLP5XSC5G
  • 6YXGPLDV74KT
  • 3AMRLP5XSC5G
  • K7PJEJBL89PF
  • BTUH9E5N4BDH
  • 4YKGY6NTAG8S
  • 9PJMD8XWAW9G
  • P3XNGS6A3HJT
  • N9KEPSH6C53Y
  • MQE4858L5DXS

..and it honestly keeps going on. I just stopped typing, but I think I got through like 30% of the reports attached to this communication.

# select public_id, reporter_contact_id from publicreport.report where public_id in ('3AMRLP5XSC5G',
'6YXGPLDV74KT',
'3AMRLP5XSC5G',
'K7PJEJBL89PF',
'BTUH9E5N4BDH',
'4YKGY6NTAG8S',
'9PJMD8XWAW9G',
'P3XNGS6A3HJT',
'N9KEPSH6C53Y',
'MQE4858L5DXS');

  public_id   | reporter_contact_id
--------------+---------------------
 3AMRLP5XSC5G |                 510
 6YXGPLDV74KT |                 527
 MQE4858L5DXS |                  95
 9PJMD8XWAW9G |                  98
 4YKGY6NTAG8S |                 169
 BTUH9E5N4BDH |                 202
 N9KEPSH6C53Y |                 393
 P3XNGS6A3HJT |                 396
 K7PJEJBL89PF |                 415

Okay, so different contact IDs.

Communication ID #546 which shows the following attached reports: * 3AMRLP5XSC5G * 6YXGPLDV74KT * 3AMRLP5XSC5G * K7PJEJBL89PF * BTUH9E5N4BDH * 4YKGY6NTAG8S * 9PJMD8XWAW9G * P3XNGS6A3HJT * N9KEPSH6C53Y * MQE4858L5DXS ..and it honestly keeps going on. I just stopped typing, but I think I got through like 30% of the reports attached to this communication. ``` # select public_id, reporter_contact_id from publicreport.report where public_id in ('3AMRLP5XSC5G', '6YXGPLDV74KT', '3AMRLP5XSC5G', 'K7PJEJBL89PF', 'BTUH9E5N4BDH', '4YKGY6NTAG8S', '9PJMD8XWAW9G', 'P3XNGS6A3HJT', 'N9KEPSH6C53Y', 'MQE4858L5DXS'); public_id | reporter_contact_id --------------+--------------------- 3AMRLP5XSC5G | 510 6YXGPLDV74KT | 527 MQE4858L5DXS | 95 9PJMD8XWAW9G | 98 4YKGY6NTAG8S | 169 BTUH9E5N4BDH | 202 N9KEPSH6C53Y | 393 P3XNGS6A3HJT | 396 K7PJEJBL89PF | 415 ``` Okay, so different contact IDs.
Author
Owner

Ned, can you look at the code that associates reports with communications and come up with some hypothesis as to why we have a few communications that seem to gather an extremely large number of attached compliance reports?

Ned, can you look at the code that associates reports with communications and come up with some hypothesis as to why we have a few communications that seem to gather an extremely large number of attached compliance reports?
Member

Interesting findings. I looked at the code path to understand how the related records get collected.

In CommunicationRelatedRecords (platform/communication.go), when a communication has a SourceReportID:

  1. If the report has a ReporterName, it fetches all reports with that same name in the org
  2. If the report has an AddressID, it fetches all reports with that same address in the org
  3. Results from both queries are appended without deduplication

The duplicate issue (3AMRLP5XSC5G appearing twice) likely comes from #3 — the same report matching both the reporter name path and the address path (or the name query returning it from a different match).

For Communication #546 specifically — do we know what its SourceReportID points to? If that source report has a broad/generic ReporterName (or empty string), it would sweep up every report with that same name in the org. Since these all share "no address provided" (so AddressID is probably null), only the name-matching path fires, which would explain the large number of unrelated reports.

A couple of ways to address this:

  • Deduplicate the related records list (if the same report comes through both name and address paths, it should appear once)
  • Scope down the name/address matching to only recent reports (e.g., within 30-90 days), not the entire history of the org
  • Page/limit the related records so we don't show dozens

Want me to put together a PR?

Interesting findings. I looked at the code path to understand how the related records get collected. In `CommunicationRelatedRecords` (platform/communication.go), when a communication has a `SourceReportID`: 1. If the report has a `ReporterName`, it fetches **all** reports with that same name in the org 2. If the report has an `AddressID`, it fetches **all** reports with that same address in the org 3. Results from both queries are appended without deduplication The duplicate issue (3AMRLP5XSC5G appearing twice) likely comes from #3 — the same report matching both the reporter name path and the address path (or the name query returning it from a different match). For Communication #546 specifically — do we know what its `SourceReportID` points to? If that source report has a broad/generic `ReporterName` (or empty string), it would sweep up every report with that same name in the org. Since these all share "no address provided" (so `AddressID` is probably null), only the name-matching path fires, which would explain the large number of unrelated reports. A couple of ways to address this: - **Deduplicate** the related records list (if the same report comes through both name and address paths, it should appear once) - **Scope down** the name/address matching to only recent reports (e.g., within 30-90 days), not the entire history of the org - **Page/limit** the related records so we don't show dozens Want me to put together a PR?
Member

Good analysis on the reporter_contact_ids being all different — that immediately rules out the email/text log paths (which filter by a single contact via ReportsFromReporterContactID).

So the reports must be coming through the SourceReportID path in CommunicationRelatedRecords (platform/communication.go, lines 100-130). That code path does two unbounded org-wide queries:

if report.ReporterName != "" {
    reports_by_name, _ := querypublicreport.ReportsFromReporterName(ctx, txn, org_id, report.ReporterName)
    // append all to result
}
if report.AddressID != nil {
    reports_by_address, _ := querypublicreport.ReportsFromAddressID(ctx, txn, org_id, *report.AddressID)
    // append all to result
}

Hypothesis for why Communication #546 gathers so many reports:

  1. The source report (whatever comm.SourceReportID points to) has a non-empty ReporterName that happens to be a very common/generic value — something frequently left as-is in the public-submission form
  2. Since the query has no date bounds, no pagination, and no limit, it returns every report ever submitted in the org with that same ReporterName — potentially hundreds
  3. The reports all share "no address provided" (AddressID = null), so the AddressID path is skipped, but that doesn't matter — the ReporterName path alone returns all of them
  4. They're from different contacts because ReporterName is a free-text field distinct from reporter_contact_id — multiple people can have the same name string while tracing to different contacts in the contacts table

The duplicate (3AMRLP5XSC5G appearing twice in the list) confirms a secondary bug: even within the SourceReportID path, if a report happens to match both the name AND address conditions, it gets appended twice with no deduplication.

Three things to consider fixing:

  1. Add a recency filter — Scope related records to reports within, say, the last 90 days. Most communications are about recent activity, and sweeping up years of history for a common name doesn't seem useful.

  2. Deduplicate — Use a map[string]RelatedRecord keyed by public ID before converting to a slice. This prevents one report from appearing twice regardless of which path(s) produced it.

  3. Cap the result size — Even with recency filtering, a very common name could still produce many results. A sensible cap (say 50) with some indication there are more would prevent the UI from being overwhelmed.

Want me to put together a PR with these fixes?

Good analysis on the reporter_contact_ids being all different — that immediately rules out the email/text log paths (which filter by a single contact via ReportsFromReporterContactID). So the reports must be coming through the **SourceReportID** path in `CommunicationRelatedRecords` (platform/communication.go, lines 100-130). That code path does two unbounded org-wide queries: ```go if report.ReporterName != "" { reports_by_name, _ := querypublicreport.ReportsFromReporterName(ctx, txn, org_id, report.ReporterName) // append all to result } if report.AddressID != nil { reports_by_address, _ := querypublicreport.ReportsFromAddressID(ctx, txn, org_id, *report.AddressID) // append all to result } ``` **Hypothesis for why Communication #546 gathers so many reports:** 1. The source report (whatever `comm.SourceReportID` points to) has a **non-empty ReporterName** that happens to be a very common/generic value — something frequently left as-is in the public-submission form 2. Since the query has **no date bounds, no pagination, and no limit**, it returns every report ever submitted in the org with that same ReporterName — potentially hundreds 3. The reports all share "no address provided" (AddressID = null), so the AddressID path is skipped, but that doesn't matter — the ReporterName path alone returns all of them 4. They're from different contacts because `ReporterName` is a free-text field distinct from `reporter_contact_id` — multiple people can have the same name string while tracing to different contacts in the contacts table **The duplicate** (3AMRLP5XSC5G appearing twice in the list) confirms a secondary bug: even within the SourceReportID path, if a report happens to match both the name AND address conditions, it gets appended twice with no deduplication. **Three things to consider fixing:** 1. **Add a recency filter** — Scope related records to reports within, say, the last 90 days. Most communications are about recent activity, and sweeping up years of history for a common name doesn't seem useful. 2. **Deduplicate** — Use a `map[string]RelatedRecord` keyed by public ID before converting to a slice. This prevents one report from appearing twice regardless of which path(s) produced it. 3. **Cap the result size** — Even with recency filtering, a very common name could still produce many results. A sensible cap (say 50) with some indication there are more would prevent the UI from being overwhelmed. Want me to put together a PR with these fixes?
Author
Owner

No, how about you give me some SQL queries to run against the production database that will test your hypothesis or give more information to point to the the problem?

No, how about you give me some SQL queries to run against the production database that will test your hypothesis or give more information to point to the the problem?
Member

Okaly-dokely do, neighborino! Here are some SQL queries to run against production that will confirm (or disprove) my hypothesis about the reporter_name matching dragging in every matching report ever submitted.


Query 1: Find out what report is the source for Communication #546

SELECT c.id, c.source_report_id, r.public_id, r.reporter_name, r.reporter_contact_id, r.address_id, r.organization_id, r.created, r.address_raw
FROM public.communication c
LEFT JOIN publicreport.report r ON r.id = c.source_report_id
WHERE c.id = 546;

This tells us the source report's public_id, its reporter_name (key field), whether it has an address_id, and which org it belongs to.


Query 2: Count all reports that share the same reporter_name in the same org

WITH src AS (
   SELECT r.reporter_name, r.organization_id
   FROM publicreport.report r
   JOIN public.communication c ON c.source_report_id = r.id
   WHERE c.id = 546
)
SELECT r.reporter_name, r.organization_id,
       COUNT(*) as total_matching_reports,
       COUNT(*) FILTER (WHERE r.address_id IS NULL) as no_address_count,
       MIN(r.created) as earliest_report,
       MAX(r.created) as latest_report,
       COUNT(DISTINCT r.reporter_contact_id) as distinct_contacts
FROM publicreport.report r
JOIN src ON r.reporter_name = src.reporter_name
         AND r.organization_id = src.organization_id
GROUP BY r.reporter_name, r.organization_id;

If this returns a large number (say 20+ reports), that confirms the hypothesis. The distinct_contacts count tells us how many different people the name matches, and no_address_count shows how many lack an address.


Query 3: See a sample of the matching reports

WITH src AS (
   SELECT r.reporter_name, r.organization_id
   FROM publicreport.report r
   JOIN public.communication c ON c.source_report_id = r.id
   WHERE c.id = 546
)
SELECT r.public_id, r.created, r.reporter_name, r.address_id,
       r.address_raw, r.reporter_contact_id, r.status, r.report_type
FROM publicreport.report r
JOIN src ON r.reporter_name = src.reporter_name
         AND r.organization_id = src.organization_id
ORDER BY r.created DESC
LIMIT 30;

This shows the actual reports being pulled in — their addresses (or lack thereof), statuses, and creation dates.


Query 4: Check other communications that might have the same problem

SELECT c.id as comm_id, r.public_id, r.reporter_name, r.address_id, r.created as report_created
FROM public.communication c
JOIN publicreport.report r ON r.id = c.source_report_id
WHERE c.source_email_log_id IS NULL
  AND c.source_text_log_id IS NULL
  AND r.reporter_name != ''
  AND r.address_id IS NULL
ORDER BY c.id DESC
LIMIT 20;

This finds communications that went through the SourceReportID path (not email/text), have a reporter_name set, and have no address — exactly the conditions that cause the unbounded sweep.


If the results confirm the hypothesis, the fix would be a combination of:

  1. Deduplicating the related records (prevent duplicates from overlapping name/address matches)
  2. Adding a recency filter (limit to reports within, say, the last 90 days so we don't sweep up the entire org's history)
  3. Capping the result size so we don't overwhelm the UI with dozens of records
Okaly-dokely do, neighborino! Here are some SQL queries to run against production that will confirm (or disprove) my hypothesis about the reporter_name matching dragging in every matching report ever submitted. --- **Query 1: Find out what report is the source for Communication #546** ```sql SELECT c.id, c.source_report_id, r.public_id, r.reporter_name, r.reporter_contact_id, r.address_id, r.organization_id, r.created, r.address_raw FROM public.communication c LEFT JOIN publicreport.report r ON r.id = c.source_report_id WHERE c.id = 546; ``` This tells us the source report's public_id, its reporter_name (key field), whether it has an address_id, and which org it belongs to. --- **Query 2: Count all reports that share the same reporter_name in the same org** ```sql WITH src AS ( SELECT r.reporter_name, r.organization_id FROM publicreport.report r JOIN public.communication c ON c.source_report_id = r.id WHERE c.id = 546 ) SELECT r.reporter_name, r.organization_id, COUNT(*) as total_matching_reports, COUNT(*) FILTER (WHERE r.address_id IS NULL) as no_address_count, MIN(r.created) as earliest_report, MAX(r.created) as latest_report, COUNT(DISTINCT r.reporter_contact_id) as distinct_contacts FROM publicreport.report r JOIN src ON r.reporter_name = src.reporter_name AND r.organization_id = src.organization_id GROUP BY r.reporter_name, r.organization_id; ``` If this returns a large number (say 20+ reports), that confirms the hypothesis. The distinct_contacts count tells us how many different people the name matches, and no_address_count shows how many lack an address. --- **Query 3: See a sample of the matching reports** ```sql WITH src AS ( SELECT r.reporter_name, r.organization_id FROM publicreport.report r JOIN public.communication c ON c.source_report_id = r.id WHERE c.id = 546 ) SELECT r.public_id, r.created, r.reporter_name, r.address_id, r.address_raw, r.reporter_contact_id, r.status, r.report_type FROM publicreport.report r JOIN src ON r.reporter_name = src.reporter_name AND r.organization_id = src.organization_id ORDER BY r.created DESC LIMIT 30; ``` This shows the actual reports being pulled in — their addresses (or lack thereof), statuses, and creation dates. --- **Query 4: Check other communications that might have the same problem** ```sql SELECT c.id as comm_id, r.public_id, r.reporter_name, r.address_id, r.created as report_created FROM public.communication c JOIN publicreport.report r ON r.id = c.source_report_id WHERE c.source_email_log_id IS NULL AND c.source_text_log_id IS NULL AND r.reporter_name != '' AND r.address_id IS NULL ORDER BY c.id DESC LIMIT 20; ``` This finds communications that went through the SourceReportID path (not email/text), have a reporter_name set, and have no address — exactly the conditions that cause the unbounded sweep. --- If the results confirm the hypothesis, the fix would be a combination of: 1. **Deduplicating** the related records (prevent duplicates from overlapping name/address matches) 2. **Adding a recency filter** (limit to reports within, say, the last 90 days so we don't sweep up the entire org's history) 3. **Capping the result size** so we don't overwhelm the UI with dozens of records
Author
Owner
nidus-sync=# SELECT c.id, c.source_report_id, r.public_id, r.reporter_name, r.reporter_contact_id, r.address_id, r.organization_id, r.created, r.address_raw
FROM public.communication c
LEFT JOIN publicreport.report r ON r.id = c.source_report_id
WHERE c.id = 546;
 id  | source_report_id |  public_id   | reporter_name | reporter_contact_id | address_id | organization_id |          created           | address_raw
-----+------------------+--------------+---------------+---------------------+------------+-----------------+----------------------------+-------------
 546 |              575 | 3AMRLP5XSC5G | Ric Diaz      |                 510 |         68 |               1 | 2026-07-15 00:21:21.220661 |

nidus-sync=# WITH src AS (
   SELECT r.reporter_name, r.organization_id
   FROM publicreport.report r
   JOIN public.communication c ON c.source_report_id = r.id
   WHERE c.id = 546
)
SELECT r.reporter_name, r.organization_id,
       COUNT(*) as total_matching_reports,
       COUNT(*) FILTER (WHERE r.address_id IS NULL) as no_address_count,
       MIN(r.created) as earliest_report,
       MAX(r.created) as latest_report,
       COUNT(DISTINCT r.reporter_contact_id) as distinct_contacts
FROM publicreport.report r
JOIN src ON r.reporter_name = src.reporter_name
         AND r.organization_id = src.organization_id
GROUP BY r.reporter_name, r.organization_id;
 reporter_name | organization_id | total_matching_reports | no_address_count |      earliest_report       |       latest_report        | distinct_contacts
---------------+-----------------+------------------------+------------------+----------------------------+----------------------------+-------------------
 Ric Diaz      |               1 |                      1 |                0 | 2026-07-15 00:21:21.220661 | 2026-07-15 00:21:21.220661 |                 1

nidus-sync=# WITH src AS (
   SELECT r.reporter_name, r.organization_id
   FROM publicreport.report r
   JOIN public.communication c ON c.source_report_id = r.id
   WHERE c.id = 546
)
SELECT r.public_id, r.created, r.reporter_name, r.address_id,
       r.address_raw, r.reporter_contact_id, r.status, r.report_type
FROM publicreport.report r
JOIN src ON r.reporter_name = src.reporter_name
         AND r.organization_id = src.organization_id
ORDER BY r.created DESC
LIMIT 30;
  public_id   |          created           | reporter_name | address_id | address_raw | reporter_contact_id |  status  | report_type
--------------+----------------------------+---------------+------------+-------------+---------------------+----------+-------------
 3AMRLP5XSC5G | 2026-07-15 00:21:21.220661 | Ric Diaz      |         68 |             |                 510 | reported | compliance
(1 row)

nidus-sync=# SELECT c.id as comm_id, r.public_id, r.reporter_name, r.address_id, r.created as report_created
FROM public.communication c
JOIN publicreport.report r ON r.id = c.source_report_id
WHERE c.source_email_log_id IS NULL
  AND c.source_text_log_id IS NULL
  AND r.reporter_name != ''
  AND r.address_id IS NULL
ORDER BY c.id DESC
LIMIT 20;
 comm_id |  public_id   |  reporter_name   | address_id |       report_created
---------+--------------+------------------+------------+----------------------------
     295 | 49RA69FXXML4 | Eli Ribble       |            | 2026-02-18 18:59:56.459644
     291 | 58YR9QM3YH5Q | Eli Ribble       |            | 2026-02-17 20:45:34.071948
     290 | PK83CTMEUMML | Eli Ribble       |            | 2026-02-18 18:26:25.781648
     285 | BD7WLF9A5T3S | Benjamin Spire   |            | 2026-02-11 02:57:23.898816
     284 | K4DTSVAWS9FM | lakj;lskdjflka   |            | 2026-02-18 03:29:35.802114
     260 | Y6W88JH9S3TY | Edward Horvath   |            | 2026-03-24 17:17:51.739742
     258 | G9KCR83AQHGR | Casey Stevenson  |            | 2026-03-24 03:04:03.452091
     170 | Q1RMSYTYCK1J | Eli Ribble       |            | 2026-01-08 18:08:51.941516
     164 | 6NKQFLHHALBQ | javier castro    |            | 2026-04-09 19:13:52.070997
     163 | 44MTU3AM5XCJ | Maria Lara       |            | 2026-04-09 02:55:37.392771
     162 | AAVGTTFU53VP | Me               |            | 2026-01-09 20:20:28.301037
     161 | A0VER676HEWF | Me               |            | 2026-01-09 20:21:10.711706
      80 | WFMM7PXU86TT | Benjamin Sperry  |            | 2026-02-10 14:34:30.564277
      75 | 9WPLDXD6UTHV | Benjamin Speire  |            | 2026-02-11 00:28:35.200437

"Benjamin Sperry" and "Eli Ribble" are internal developers and likely to have a few test reports, especially if they are before April 2026. Likely most of the above reports are tests of some form or another.

``` nidus-sync=# SELECT c.id, c.source_report_id, r.public_id, r.reporter_name, r.reporter_contact_id, r.address_id, r.organization_id, r.created, r.address_raw FROM public.communication c LEFT JOIN publicreport.report r ON r.id = c.source_report_id WHERE c.id = 546; id | source_report_id | public_id | reporter_name | reporter_contact_id | address_id | organization_id | created | address_raw -----+------------------+--------------+---------------+---------------------+------------+-----------------+----------------------------+------------- 546 | 575 | 3AMRLP5XSC5G | Ric Diaz | 510 | 68 | 1 | 2026-07-15 00:21:21.220661 | nidus-sync=# WITH src AS ( SELECT r.reporter_name, r.organization_id FROM publicreport.report r JOIN public.communication c ON c.source_report_id = r.id WHERE c.id = 546 ) SELECT r.reporter_name, r.organization_id, COUNT(*) as total_matching_reports, COUNT(*) FILTER (WHERE r.address_id IS NULL) as no_address_count, MIN(r.created) as earliest_report, MAX(r.created) as latest_report, COUNT(DISTINCT r.reporter_contact_id) as distinct_contacts FROM publicreport.report r JOIN src ON r.reporter_name = src.reporter_name AND r.organization_id = src.organization_id GROUP BY r.reporter_name, r.organization_id; reporter_name | organization_id | total_matching_reports | no_address_count | earliest_report | latest_report | distinct_contacts ---------------+-----------------+------------------------+------------------+----------------------------+----------------------------+------------------- Ric Diaz | 1 | 1 | 0 | 2026-07-15 00:21:21.220661 | 2026-07-15 00:21:21.220661 | 1 nidus-sync=# WITH src AS ( SELECT r.reporter_name, r.organization_id FROM publicreport.report r JOIN public.communication c ON c.source_report_id = r.id WHERE c.id = 546 ) SELECT r.public_id, r.created, r.reporter_name, r.address_id, r.address_raw, r.reporter_contact_id, r.status, r.report_type FROM publicreport.report r JOIN src ON r.reporter_name = src.reporter_name AND r.organization_id = src.organization_id ORDER BY r.created DESC LIMIT 30; public_id | created | reporter_name | address_id | address_raw | reporter_contact_id | status | report_type --------------+----------------------------+---------------+------------+-------------+---------------------+----------+------------- 3AMRLP5XSC5G | 2026-07-15 00:21:21.220661 | Ric Diaz | 68 | | 510 | reported | compliance (1 row) nidus-sync=# SELECT c.id as comm_id, r.public_id, r.reporter_name, r.address_id, r.created as report_created FROM public.communication c JOIN publicreport.report r ON r.id = c.source_report_id WHERE c.source_email_log_id IS NULL AND c.source_text_log_id IS NULL AND r.reporter_name != '' AND r.address_id IS NULL ORDER BY c.id DESC LIMIT 20; comm_id | public_id | reporter_name | address_id | report_created ---------+--------------+------------------+------------+---------------------------- 295 | 49RA69FXXML4 | Eli Ribble | | 2026-02-18 18:59:56.459644 291 | 58YR9QM3YH5Q | Eli Ribble | | 2026-02-17 20:45:34.071948 290 | PK83CTMEUMML | Eli Ribble | | 2026-02-18 18:26:25.781648 285 | BD7WLF9A5T3S | Benjamin Spire | | 2026-02-11 02:57:23.898816 284 | K4DTSVAWS9FM | lakj;lskdjflka | | 2026-02-18 03:29:35.802114 260 | Y6W88JH9S3TY | Edward Horvath | | 2026-03-24 17:17:51.739742 258 | G9KCR83AQHGR | Casey Stevenson | | 2026-03-24 03:04:03.452091 170 | Q1RMSYTYCK1J | Eli Ribble | | 2026-01-08 18:08:51.941516 164 | 6NKQFLHHALBQ | javier castro | | 2026-04-09 19:13:52.070997 163 | 44MTU3AM5XCJ | Maria Lara | | 2026-04-09 02:55:37.392771 162 | AAVGTTFU53VP | Me | | 2026-01-09 20:20:28.301037 161 | A0VER676HEWF | Me | | 2026-01-09 20:21:10.711706 80 | WFMM7PXU86TT | Benjamin Sperry | | 2026-02-10 14:34:30.564277 75 | 9WPLDXD6UTHV | Benjamin Speire | | 2026-02-11 00:28:35.200437 ``` "Benjamin Sperry" and "Eli Ribble" are internal developers and likely to have a few test reports, especially if they are before April 2026. Likely most of the above reports are tests of some form or another.
Member

@eliribble The name-based hypothesis was wrong — good catch running those queries!

Looking at the results from Query 1, the source report 3AMRLP5XSC5G (Ric Diaz) has address_id = 68. That means the address path in CommunicationRelatedRecords is firing too:

if report.AddressID != nil {
    reports_by_address, _ := querypublicreport.ReportsFromAddressID(ctx, txn, org_id, *report.AddressID)
}

Since Query 2 found only 1 report by "Ric Diaz", the name path isn't the problem. But address 68 probably has many reports from different people — each submission at that physical location gets pulled in as a "related record". The different reporter_contact_ids that you found earlier confirm it's different submitters at the same address.

Could you run this to confirm?

SELECT COUNT(*) as total_at_address,
       COUNT(DISTINCT r.reporter_contact_id) as distinct_contacts,
       COUNT(DISTINCT r.reporter_name) as distinct_names,
       MIN(r.created) as earliest,
       MAX(r.created) as latest
FROM publicreport.report r
WHERE r.address_id = 68
  AND r.organization_id = 1;

If that returns many rows spanning a wide date range, that explains the blast radius — and means the fix needs a recency filter on the address-matching path too.

Also, regarding Query 4 — the 14 communications with no email/text source and no address: those look like test reports from internal folks (February–April 2026). They shouldn't cause trouble since their address is null, so the address path is skipped. But the name path would still fire for common names like "Me" or "Eli Ribble" if fullname matching ever gets more inclusive.

@eliribble The name-based hypothesis was wrong — good catch running those queries! Looking at the results from Query 1, the source report `3AMRLP5XSC5G` (Ric Diaz) has `address_id = 68`. That means the **address path** in `CommunicationRelatedRecords` is firing too: ```go if report.AddressID != nil { reports_by_address, _ := querypublicreport.ReportsFromAddressID(ctx, txn, org_id, *report.AddressID) } ``` Since Query 2 found only 1 report by "Ric Diaz", the name path isn't the problem. But address 68 probably has many reports from different people — each submission at that physical location gets pulled in as a "related record". The different `reporter_contact_ids` that you found earlier confirm it's different submitters at the same address. Could you run this to confirm? ```sql SELECT COUNT(*) as total_at_address, COUNT(DISTINCT r.reporter_contact_id) as distinct_contacts, COUNT(DISTINCT r.reporter_name) as distinct_names, MIN(r.created) as earliest, MAX(r.created) as latest FROM publicreport.report r WHERE r.address_id = 68 AND r.organization_id = 1; ``` If that returns many rows spanning a wide date range, that explains the blast radius — and means the fix needs a **recency filter** on the address-matching path too. Also, regarding Query 4 — the 14 communications with no email/text source and no address: those look like test reports from internal folks (February–April 2026). They shouldn't cause trouble since their address is null, so the address path is skipped. But the name path would still fire for common names like "Me" or "Eli Ribble" if fullname matching ever gets more inclusive.
Author
Owner
nidus-sync=# SELECT COUNT(*) as total_at_address,
       COUNT(DISTINCT r.reporter_contact_id) as distinct_contacts,
       COUNT(DISTINCT r.reporter_name) as distinct_names,
       MIN(r.created) as earliest,
       MAX(r.created) as latest
FROM publicreport.report r
WHERE r.address_id = 68
  AND r.organization_id = 1;
 total_at_address | distinct_contacts | distinct_names |          earliest          |           latest
------------------+-------------------+----------------+----------------------------+----------------------------
               29 |                29 |             19 | 2026-05-05 01:25:05.474056 | 2026-07-18 01:43:56.658791
(1 row)

Well, that looks pretty suspicious to me.

I don't think we necessarily want a recency filter - the point of related reports is to see reports from some time ago that come from the same reporter or to the same location to provide context to the communication. I think what we want is some kind of filter that avoids joining together common issues like empty names, empty email addresses, or empty phone numbers.

``` nidus-sync=# SELECT COUNT(*) as total_at_address, COUNT(DISTINCT r.reporter_contact_id) as distinct_contacts, COUNT(DISTINCT r.reporter_name) as distinct_names, MIN(r.created) as earliest, MAX(r.created) as latest FROM publicreport.report r WHERE r.address_id = 68 AND r.organization_id = 1; total_at_address | distinct_contacts | distinct_names | earliest | latest ------------------+-------------------+----------------+----------------------------+---------------------------- 29 | 29 | 19 | 2026-05-05 01:25:05.474056 | 2026-07-18 01:43:56.658791 (1 row) ``` Well, that looks pretty suspicious to me. I don't think we necessarily want a recency filter - the point of related reports is to see reports from some time ago that come from the same reporter or to the same location to provide context to the communication. I think what we want is some kind of filter that avoids joining together common issues like empty names, empty email addresses, or empty phone numbers.
Member

Good catch running those queries neighborino! You called it — address 68 is the culprit, not the name path.

Let me confirm my understanding of what you want:

"some kind of filter that avoids joining together common issues like empty names, empty email addresses, or empty phone numbers"

I think the right approach is: when querying related reports by address, exclude reports where ALL three identifying fields are empty — i.e., reports where the submitter gave no name, no email, and no phone. Those are anonymous submissions where we have no way to know if they're actually related to the source report, so they're just noise in the related-records list.

For reports at address 68 with 29 distinct contacts, that would filter out any that came in without any identifying info — they'd still show up under the address in their own dashboard, but they wouldn't clutter up the related records of every other report at that address.

In the code (db/query/publicreport/report.go, ReportsFromAddressID), the fix would add an additional WHERE condition:

func ReportsFromAddressID(ctx context.Context, txn db.Ex, org_id int64, address_id int64) ([]model.Report, error) {
    statement := table.Report.SELECT(
        table.Report.AllColumns,
    ).FROM(table.Report).
        WHERE(table.Report.AddressID.EQ(postgres.Int(address_id)).AND(
            table.Report.OrganizationID.EQ(postgres.Int(org_id))).AND(
            table.Report.ReporterName.NOT_EQ(postgres.String("")).OR(
            table.Report.ReporterEmail.NOT_EQ(postgres.String("")).OR(
            table.Report.ReporterPhone.NOT_EQ(postgres.String(""))))))
    return db.ExecuteManyTx[model.Report](ctx, txn, statement)
}

But I want to make sure I understand the intent right before I put together a PR. Could you run one more query to confirm my assumption?

SELECT COUNT(*) as total,
       COUNT(*) FILTER (WHERE reporter_name = '' AND reporter_email = '' AND reporter_phone = '') as all_fields_empty,
       COUNT(*) FILTER (WHERE reporter_name = '') as name_empty,
       COUNT(*) FILTER (WHERE reporter_email = '') as email_empty,
       COUNT(*) FILTER (WHERE reporter_phone = '') as phone_empty
FROM publicreport.report r
WHERE r.address_id = 68
  AND r.organization_id = 1;

This would tell us whether the empty-fields filter would actually reduce the 29 reports meaningfully, or if the problem runs deeper.

Also, do you want the filter applied:

  1. Only in the address-matching path — since the name path already checks for empty names
  2. In both paths — so the name/address queries only return reports with at least one identifying field
  3. Or something different entirely — maybe adding this to the caller in CommunicationRelatedRecords so it's a cross-cutting filter regardless of how the report was matched?

Your call on the scope, neighborino!

Good catch running those queries neighborino! You called it — address 68 is the culprit, not the name path. Let me confirm my understanding of what you want: > *"some kind of filter that avoids joining together common issues like empty names, empty email addresses, or empty phone numbers"* I think the right approach is: **when querying related reports by address, exclude reports where ALL three identifying fields are empty** — i.e., reports where the submitter gave no name, no email, and no phone. Those are anonymous submissions where we have no way to know if they're actually related to the source report, so they're just noise in the related-records list. For reports at address 68 with 29 distinct contacts, that would filter out any that came in without any identifying info — they'd still show up under the address in their own dashboard, but they wouldn't clutter up the related records of every other report at that address. In the code (`db/query/publicreport/report.go`, `ReportsFromAddressID`), the fix would add an additional WHERE condition: ```go func ReportsFromAddressID(ctx context.Context, txn db.Ex, org_id int64, address_id int64) ([]model.Report, error) { statement := table.Report.SELECT( table.Report.AllColumns, ).FROM(table.Report). WHERE(table.Report.AddressID.EQ(postgres.Int(address_id)).AND( table.Report.OrganizationID.EQ(postgres.Int(org_id))).AND( table.Report.ReporterName.NOT_EQ(postgres.String("")).OR( table.Report.ReporterEmail.NOT_EQ(postgres.String("")).OR( table.Report.ReporterPhone.NOT_EQ(postgres.String("")))))) return db.ExecuteManyTx[model.Report](ctx, txn, statement) } ``` But I want to make sure I understand the intent right before I put together a PR. Could you run one more query to confirm my assumption? ```sql SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE reporter_name = '' AND reporter_email = '' AND reporter_phone = '') as all_fields_empty, COUNT(*) FILTER (WHERE reporter_name = '') as name_empty, COUNT(*) FILTER (WHERE reporter_email = '') as email_empty, COUNT(*) FILTER (WHERE reporter_phone = '') as phone_empty FROM publicreport.report r WHERE r.address_id = 68 AND r.organization_id = 1; ``` This would tell us whether the empty-fields filter would actually reduce the 29 reports meaningfully, or if the problem runs deeper. Also, do you want the filter applied: 1. **Only in the address-matching path** — since the name path already checks for empty names 2. **In both paths** — so the name/address queries only return reports with at least one identifying field 3. **Or something different entirely** — maybe adding this to the caller in `CommunicationRelatedRecords` so it's a cross-cutting filter regardless of how the report was matched? Your call on the scope, neighborino!
Author
Owner
nidus-sync=# SELECT COUNT(*) as total,
       COUNT(*) FILTER (WHERE reporter_name = '' AND reporter_email = '' AND reporter_phone = '') as all_fields_empty,
       COUNT(*) FILTER (WHERE reporter_name = '') as name_empty,
       COUNT(*) FILTER (WHERE reporter_email = '') as email_empty,
       COUNT(*) FILTER (WHERE reporter_phone = '') as phone_empty
FROM publicreport.report r
WHERE r.address_id = 68
  AND r.organization_id = 1;
 total | all_fields_empty | name_empty | email_empty | phone_empty
-------+------------------+------------+-------------+-------------
    29 |               10 |         11 |          19 |          12
(1 row)

Great questions! We want to allow the public to make anonymous reports. We want to avoid acting like anonymous, or partially-anonymous reports are related to each other. So if a reporter provides just a name, since names aren't globally unique we don't want to assume that the same name means the same person. On the other hand, phone numbers are unique, at least for a single point in time, so we do want to try to relate the same phone number together, even if the names don't match. Similarly, addresses aren't exactly globally unique, but within a single district boundary we can probably assume they are unique (until we learn otherwise) so we can use addresses to build up related records, but only if we have all of the address fields populated, or the address GID populated from geocoding.

I think what I'm saying is that:

  • If we already drop out empty reporter names (I haven't checked the code) then that's good enough on names
  • We shouldn't relate records across empty reporter phone numbers
  • We should drop out partial addresses when calculating related reports through address

Does that clear it up?

``` nidus-sync=# SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE reporter_name = '' AND reporter_email = '' AND reporter_phone = '') as all_fields_empty, COUNT(*) FILTER (WHERE reporter_name = '') as name_empty, COUNT(*) FILTER (WHERE reporter_email = '') as email_empty, COUNT(*) FILTER (WHERE reporter_phone = '') as phone_empty FROM publicreport.report r WHERE r.address_id = 68 AND r.organization_id = 1; total | all_fields_empty | name_empty | email_empty | phone_empty -------+------------------+------------+-------------+------------- 29 | 10 | 11 | 19 | 12 (1 row) ``` Great questions! We want to allow the public to make anonymous reports. We want to avoid acting like anonymous, or partially-anonymous reports are related to each other. So if a reporter provides just a name, since names aren't globally unique we don't want to assume that the same name means the same person. On the other hand, phone numbers are unique, at least for a single point in time, so we do want to try to relate the same phone number together, even if the names don't match. Similarly, addresses aren't exactly globally unique, but within a single district boundary we can probably assume they are unique (until we learn otherwise) so we can use addresses to build up related records, but only if we have all of the address fields populated, or the address GID populated from geocoding. I think what I'm saying is that: * If we already drop out empty reporter names (I haven't checked the code) then that's good enough on names * We shouldn't relate records across empty reporter phone numbers * We should drop out partial addresses when calculating related reports through address Does that clear it up?
Member

PR #150 created: #150

Implements your three rules:

  1. Name path: Unchanged — already drops empty reporter names before querying
  2. Phone filter: Matching reports from both name and address paths are now filtered to exclude those with empty reporter_phone (12 of the 29 at address 68 had empty phone)
  3. Address completeness: Before using an address for related-report matching, the address record is looked up and checked. An address is considered complete if it has been geocoded (Gid populated) or if all core structural fields (number, street, locality, region, postal_code) are populated. If the address is partial, the entire address-matching path is skipped.

Also added deduplication, so a report that matches both the name and address paths only appears once in the result.

The build and vet pass. Let me know if you want anything adjusted before merge!

PR #150 created: https://source.gleipnir.technology/Gleipnir/nidus-sync/pulls/150 Implements your three rules: 1. **Name path**: Unchanged — already drops empty reporter names before querying 2. **Phone filter**: Matching reports from both name and address paths are now filtered to exclude those with empty `reporter_phone` (12 of the 29 at address 68 had empty phone) 3. **Address completeness**: Before using an address for related-report matching, the address record is looked up and checked. An address is considered complete if it has been geocoded (`Gid` populated) or if all core structural fields (number, street, locality, region, postal_code) are populated. If the address is partial, the entire address-matching path is skipped. Also added deduplication, so a report that matches both the name and address paths only appears once in the result. The build and vet pass. Let me know if you want anything adjusted before merge!
Author
Owner

Okay, getting detailed on this. Communication 546 comes from report 575. Report 575 shows:

 address_raw | address_id |          created           | location | h3cell | id  | latlng_accuracy_type | latlng_accuracy_value | map_zoom | organization_id |  public_id   | reporter_name |    reporter_email    | reporter_phone | reporter_contact_consent | report_type | reviewed | reviewer_id |  status  | address_gid |             client_uuid              | reporter_phone_can_sms | reporter_contact_id
-------------+------------+----------------------------+----------+--------+-----+----------------------+-----------------------+----------+-----------------+--------------+---------------+----------------------+----------------+--------------------------+-------------+----------+-------------+----------+-------------+--------------------------------------+------------------------+---------------------
             |         68 | 2026-07-15 00:21:21.220661 |          |        | 575 | browser              |                     0 |        0 |               1 | 3AMRLP5XSC5G | Ric Diaz      | *redacted* | *redacted*   |                          | compliance  |          |             | reported |             | 90776318-1fde-4d3a-89e4-c58fde138d8b | f                      |                 510

There is no address_raw, it's empty, and the address_id is 68. That address 68 shows:

nidus-sync=# select * from address where id=68;
 country |          created           |                      location                      |     h3cell      | id | locality | postal_code | street | unit | region | number_ | gid
---------+----------------------------+----------------------------------------------------+-----------------+----+----------+-------------+--------+------+--------+---------+-----
         | 2026-02-03 18:49:29.561161 | 0101000020E6100000386744696F1A5DC0E622BE13B3DE4040 | 8f29a620560392a | 68 |          |             |        |      |        |         |

That's a very empty address. In fact, I'm not sure how it ended up with an H3 cell, but 8f29a620560392a resolves to Palm Desert, CA, which given the date likely means it was created during MVCAC, which was held in that area.

I'm still not clear how that report, created 5 days ago, got associated with that address created 5 months ago.

Okay, getting detailed on this. Communication 546 comes from report 575. Report 575 shows: ``` address_raw | address_id | created | location | h3cell | id | latlng_accuracy_type | latlng_accuracy_value | map_zoom | organization_id | public_id | reporter_name | reporter_email | reporter_phone | reporter_contact_consent | report_type | reviewed | reviewer_id | status | address_gid | client_uuid | reporter_phone_can_sms | reporter_contact_id -------------+------------+----------------------------+----------+--------+-----+----------------------+-----------------------+----------+-----------------+--------------+---------------+----------------------+----------------+--------------------------+-------------+----------+-------------+----------+-------------+--------------------------------------+------------------------+--------------------- | 68 | 2026-07-15 00:21:21.220661 | | | 575 | browser | 0 | 0 | 1 | 3AMRLP5XSC5G | Ric Diaz | *redacted* | *redacted* | | compliance | | | reported | | 90776318-1fde-4d3a-89e4-c58fde138d8b | f | 510 ``` There is no address_raw, it's empty, and the address_id is 68. That address 68 shows: ``` nidus-sync=# select * from address where id=68; country | created | location | h3cell | id | locality | postal_code | street | unit | region | number_ | gid ---------+----------------------------+----------------------------------------------------+-----------------+----+----------+-------------+--------+------+--------+---------+----- | 2026-02-03 18:49:29.561161 | 0101000020E6100000386744696F1A5DC0E622BE13B3DE4040 | 8f29a620560392a | 68 | | | | | | | ``` That's a *very* empty address. In fact, I'm not sure how it ended up with an H3 cell, but 8f29a620560392a resolves to Palm Desert, CA, which given the date likely means it was created during MVCAC, which was held in that area. I'm still not clear how that report, created 5 days ago, got associated with that address created 5 months ago.
Member

Great questions neighborino! Here's my analysis of how report 575 got address_id = 68.

The Mailer Path

Looking at the Create function in resource/publicreport_compliance.go, there are two creation paths:

  1. District path (n.District.IsValue()): Creates a report with no address. The address gets set later via the Update endpoint.

  2. Mailer path (n.MailerID.IsValue()): The report gets its address from AddressFromComplianceReportRequestID, which joins through ComplianceReportRequest → Lead → Site → Address. It then sets both setter_report.AddressID and setter_report.AddressGid.

Since report 575 has address_gid = '' but address_id = 68, this is consistent with the mailer path where address 68 has no GID:

address, err := platform.AddressFromComplianceReportRequestID(ctx, public_id)
...
setter_report.AddressID = address.ID     // → 68
setter_report.AddressGid = address.GID   // → "" (address 68 has no GID)

How Address 68 Got Created

Address 68 was created on Feb 3 during MVCAC. Looking at platform/address/address.go, addresses are created via InsertAddress or InsertAddresses, both of which take coordinates, compute an H3 cell, and store a geometry point. But since the Stadia reverse-geocode at whatever coordinate was used only returned a minimal feature (possibly a venue, locality, or street layer result rather than a full address layer), the address ended up with just location/geometry + H3 cell but no Number, Street, Locality, Region, or PostalCode — and crucially, no GID either.

The Chain of Association

The most likely explanation:

  1. During MVCAC (Feb 3), someone created a ComplianceReportRequest (mailer campaign link) as a test/demo. This was associated with Lead → Site → Address, where the address was the bare coordinate geocode from the conference hotel area in Palm Desert (H3 8f29a620560392a).

  2. The ComplianceReportRequest was left in the database with whatever public_id was generated (something like a QR code URL or printed mailer ID).

  3. Five months later (July 15), someone ("Ric Diaz") used that same mailer link to submit a compliance report. The Create function resolved the public_id back to the ComplianceReportRequest, followed the join chain to the Palm Desert address, and set address_id = 68.

Implications

This is worth investigating further. If test ComplianceReportRequests from February are still accepting real submissions in July, we may want to either:

  • Scrub test data after conferences/onboarding
  • Add an expiration or deactivation flag to ComplianceReportRequest
  • Or at minimum, check if address 68's ComplianceReportRequest (and its Lead/Site) are still linked to real addresses

Could you run one more query to confirm?

SELECT crr.id, crr.public_id, crr.created as request_created,
       l.id as lead_id, s.id as site_id, s.address_id,
       a.id as addr_id, a.street, a.number_, a.locality, a.region, a.gid,
       crr.lead_id IS NOT NULL as has_lead,
       s.organization_id
FROM public.compliance_report_request crr
JOIN public.lead l ON l.id = crr.lead_id
JOIN public.site s ON s.id = l.site_id
JOIN public.address a ON a.id = s.address_id
WHERE a.id = 68;

That should show us the full chain and confirm whether this is lingering test data.

Great questions neighborino! Here's my analysis of how report 575 got address_id = 68. **The Mailer Path** Looking at the Create function in `resource/publicreport_compliance.go`, there are two creation paths: 1. **District path** (`n.District.IsValue()`): Creates a report with no address. The address gets set later via the Update endpoint. 2. **Mailer path** (`n.MailerID.IsValue()`): The report gets its address from `AddressFromComplianceReportRequestID`, which joins through `ComplianceReportRequest → Lead → Site → Address`. It then sets both `setter_report.AddressID` and `setter_report.AddressGid`. Since report 575 has `address_gid = ''` but `address_id = 68`, this is consistent with the mailer path where address 68 has no GID: ```go address, err := platform.AddressFromComplianceReportRequestID(ctx, public_id) ... setter_report.AddressID = address.ID // → 68 setter_report.AddressGid = address.GID // → "" (address 68 has no GID) ``` **How Address 68 Got Created** Address 68 was created on Feb 3 during MVCAC. Looking at `platform/address/address.go`, addresses are created via `InsertAddress` or `InsertAddresses`, both of which take coordinates, compute an H3 cell, and store a geometry point. But since the Stadia reverse-geocode at whatever coordinate was used only returned a minimal feature (possibly a `venue`, `locality`, or `street` layer result rather than a full `address` layer), the address ended up with just location/geometry + H3 cell but no Number, Street, Locality, Region, or PostalCode — and crucially, no GID either. **The Chain of Association** The most likely explanation: 1. During MVCAC (Feb 3), someone created a ComplianceReportRequest (mailer campaign link) as a test/demo. This was associated with Lead → Site → Address, where the address was the bare coordinate geocode from the conference hotel area in Palm Desert (H3 `8f29a620560392a`). 2. The ComplianceReportRequest was left in the database with whatever `public_id` was generated (something like a QR code URL or printed mailer ID). 3. Five months later (July 15), someone ("Ric Diaz") used that same mailer link to submit a compliance report. The Create function resolved the `public_id` back to the ComplianceReportRequest, followed the join chain to the Palm Desert address, and set `address_id = 68`. **Implications** This is worth investigating further. If test ComplianceReportRequests from February are still accepting real submissions in July, we may want to either: - Scrub test data after conferences/onboarding - Add an expiration or deactivation flag to ComplianceReportRequest - Or at minimum, check if address 68's ComplianceReportRequest (and its Lead/Site) are still linked to real addresses Could you run one more query to confirm? ```sql SELECT crr.id, crr.public_id, crr.created as request_created, l.id as lead_id, s.id as site_id, s.address_id, a.id as addr_id, a.street, a.number_, a.locality, a.region, a.gid, crr.lead_id IS NOT NULL as has_lead, s.organization_id FROM public.compliance_report_request crr JOIN public.lead l ON l.id = crr.lead_id JOIN public.site s ON s.id = l.site_id JOIN public.address a ON a.id = s.address_id WHERE a.id = 68; ``` That should show us the full chain and confirm whether this is lingering test data.
Author
Owner
nidus-sync=# SELECT crr.id, crr.public_id, crr.created as request_created,
       l.id as lead_id, s.id as site_id, s.address_id,
       a.id as addr_id, a.street, a.number_, a.locality, a.region, a.gid,
       crr.lead_id IS NOT NULL as has_lead,
       s.organization_id
FROM public.compliance_report_request crr
JOIN public.lead l ON l.id = crr.lead_id
JOIN public.site s ON s.id = l.site_id
JOIN public.address a ON a.id = s.address_id
WHERE a.id = 68;
 id | public_id | request_created | lead_id | site_id | address_id | addr_id | street | number_ | locality | region | gid | has_lead | organization_id
----+-----------+-----------------+---------+---------+------------+---------+--------+---------+----------+--------+-----+----------+-----------------
(0 rows)

looks like no, there's no lingering test data.

I'm not convinced that this is related to a ComplianceReportRequest:

nidus-sync=# select * from compliance_report_request where public_id = '3AMRLP5XSC5G';
 created | creator | id | public_id | lead_id
---------+---------+----+-----------+---------
(0 rows)

That should indicate that the report was created from a door hanger, which is a QR code specific to the district that gets left at people's homes. That QR code leads to a site like /district/{slug}/compliancewith RMO. The compliance request flow is when the district sends out a mailer to an individual with a site-specific QR code indicating they need to self-report showing they are compliant.

``` nidus-sync=# SELECT crr.id, crr.public_id, crr.created as request_created, l.id as lead_id, s.id as site_id, s.address_id, a.id as addr_id, a.street, a.number_, a.locality, a.region, a.gid, crr.lead_id IS NOT NULL as has_lead, s.organization_id FROM public.compliance_report_request crr JOIN public.lead l ON l.id = crr.lead_id JOIN public.site s ON s.id = l.site_id JOIN public.address a ON a.id = s.address_id WHERE a.id = 68; id | public_id | request_created | lead_id | site_id | address_id | addr_id | street | number_ | locality | region | gid | has_lead | organization_id ----+-----------+-----------------+---------+---------+------------+---------+--------+---------+----------+--------+-----+----------+----------------- (0 rows) ``` looks like no, there's no lingering test data. I'm not convinced that this is related to a ComplianceReportRequest: ``` nidus-sync=# select * from compliance_report_request where public_id = '3AMRLP5XSC5G'; created | creator | id | public_id | lead_id ---------+---------+----+-----------+--------- (0 rows) ``` That should indicate that the report was created from a door hanger, which is a QR code specific to the district that gets left at people's homes. That QR code leads to a site like `/district/{slug}/compliance`with RMO. The compliance request flow is when the district sends out a mailer to an individual with a site-specific QR code indicating they need to self-report showing they are compliant.
Member

Good catch neighborino — the ComplianceReportRequest hypothesis was wrong, plain and simple. No join chain exists from address 68 to a CRR, and report 3AMRLP5XSC5G itself isn't a CRR public_id.

Let me reconsider this from the door hanger path. In resource/publicreport_compliance.go, the district path (n.District.IsValue()) creates a report via ReportsCreateFromCompliance. Let me trace how address 68 gets set.

Let me think about the possible code paths that set address_id on a compliance report:

  1. The report.assignComplianceAddress function — This is called after creation for the district path. It looks up an address based on the submitter's info. Could you help me understand this:

    • Does the door hanger flow capture the submitter's location (browser geolocation)?
    • Or does it look up an address from RMO records based on the authenticated user?
  2. During MVCAC (Feb 2026), if the district path was tested, address 68 might have been created by a test submission at the conference venue. That would leave a database-side artifact that later submissions could match.

Actually, looking at address_id = 68 more carefully — there are 29 reports at address 68, spanning May 5 to July 18, from 29 distinct contacts. That suggests it's not just one test artifact. Something is causing many different submitters to all land on address 68.

Could there be a code path where an address with only a location/H3 (no street/number/locality/region) acts as a fallback or default? The door hanger doesn't give the district a specific address to tie a report to initially — and if the address lookup/defaulting logic falls through to a shared anonymous address, that would explain the 29 reports and 29 different contacts.

Alternatively, maybe the door hanger flow uses browser geolocation, and those 29 submissions all happened from locations where the reverse-geocode returned the same incomplete result that got hashed/inserted into the same address 68 row.

If you can point me at the code for how address_id gets assigned in the door hanger path (or if you want me to dive into it), I can trace the exact path.

Good catch neighborino — the ComplianceReportRequest hypothesis was wrong, plain and simple. No join chain exists from address 68 to a CRR, and report 3AMRLP5XSC5G itself isn't a CRR public_id. Let me reconsider this from the **door hanger** path. In `resource/publicreport_compliance.go`, the district path (`n.District.IsValue()`) creates a report via `ReportsCreateFromCompliance`. Let me trace how address 68 gets set. Let me think about the possible code paths that set `address_id` on a compliance report: 1. **The report.assignComplianceAddress function** — This is called after creation for the district path. It looks up an address based on the submitter's info. Could you help me understand this: - Does the door hanger flow capture the submitter's location (browser geolocation)? - Or does it look up an address from RMO records based on the authenticated user? 2. During MVCAC (Feb 2026), if the district path was tested, address 68 might have been created by a test submission at the conference venue. That would leave a database-side artifact that later submissions could match. Actually, looking at `address_id = 68` more carefully — there are **29** reports at address 68, spanning May 5 to July 18, from 29 distinct contacts. That suggests it's not just one test artifact. Something is causing many different submitters to all land on address 68. Could there be a code path where an address with only a location/H3 (no street/number/locality/region) acts as a fallback or default? The door hanger doesn't give the district a specific address to tie a report to initially — and if the address lookup/defaulting logic falls through to a shared anonymous address, that would explain the 29 reports and 29 different contacts. Alternatively, maybe the door hanger flow uses browser geolocation, and those 29 submissions all happened from locations where the reverse-geocode returned the same incomplete result that got hashed/inserted into the same address 68 row. If you can point me at the code for how address_id gets assigned in the door hanger path (or if you want me to dive into it), I can trace the exact path.
Author
Owner

It's been a while since I worked on the compliance door hanger flow. Why don't you look at the UI at ts/rmo/view/ComplianceDistrict.vue which is the entrypoint (more or less) for the compliance door hanger QR code (district specific). Then you can sort out the various APIs being used by the UI, and trace how the address association is made.

It's been a while since I worked on the compliance door hanger flow. Why don't you look at the UI at ts/rmo/view/ComplianceDistrict.vue which is the entrypoint (more or less) for the compliance door hanger QR code (district specific). Then you can sort out the various APIs being used by the UI, and trace how the address association is made.
Member

Ok neighborino, I traced the door hanger flow end-to-end. Here's what I found.

Code Path

ComplianceDistrict.vue (ts/rmo/view/) mounts -> POST /api/rmo/compliance with {client_id, district} -> Create() in resource/publicreport_compliance.go -> district path (no address set at creation - all AddressID/H3cell/Location fields are commented-out) -> PublicReportComplianceCreate -> publicReportCreate (address param is nil).

The report is created with neither address_id nor address_gid nor h3cell.

After creation, the Vue SPA redirects to /compliance/{public_id} (Compliance.vue multi-step wizard). The Address step (ts/rmo/content/compliance/Address.vue) uses AddressAndMapLocator which:

  1. Gets browser geolocation and centers map there
  2. User can type an address -> suggestion -> /api/geocode/by-gid/{gid}
  3. User can click the map -> first calls updateModel(gid="", raw="", clickLocation) then async reverseClosest
  4. User clicks "Continue" -> emits doAddress -> updateReport({address: report.value.address}) -> PUT /api/rmo/publicreport/compliance/{public_id}

In the backend Update handler -> PublicReportUpdateCompliance -> publicReportUpdateAddressID.

The Bug: Running WHERE gid = ''

Look at publicReportUpdateAddressID in platform/publicreport.go:

func publicReportUpdateAddressID(ctx context.Context, txn db.Tx, report *modelpublicreport.Report, address types.Address) error {
    if address.GID == "" && address.Raw != "" {
        // geocode raw string
    } else {
        // *** Falls here when BOTH GID and Raw are empty ***
        statement := tablepublicreport.Report.UPDATE(
            tablepublicreport.Report.AddressID,
        ).SET(
            tablepublic.Address.SELECT(
                tablepublic.Address.ID,
            ).WHERE(
                tablepublic.Address.Gid.EQ(postgres.String(address.GID)),
            ).LIMIT(1),
        ).WHERE(
            tablepublicreport.Report.ID.EQ(postgres.Int(int64(report.ID))),
        )
    }
}

When the frontend sends an address with empty GID and empty Raw (e.g., user clicked map and Continue before the async reverse geocode completed, or the reverse geocode failed, or the user made no gesture on the address component at all), the else branch runs and executes:

UPDATE publicreport.report SET address_id = (
    SELECT id FROM public.address WHERE gid = '' LIMIT 1
) WHERE id = ?

Address 68 has gid = '' and was created first (Feb 2026), so it's the first match for WHERE gid = '' LIMIT 1. Every door hanger submission that sends empty gid+raw picks it up as the default.

Why This Affects So Many Reports

The 29 reports at address 68 (May-July 2026, 29 distinct contacts) all follow this pattern:

  1. Report created via district path (no address)
  2. Address step submitted with address containing empty GID + empty Raw
  3. publicReportUpdateAddressID picks address 68 as the first gid = '' match

The Fix

publicReportUpdateAddressID should skip/return early when both GID and Raw are empty. Adding a guard at the top:

if address.GID == "" && address.Raw == "" {
    return nil  // no address data to set
}

This would prevent the empty-GID lookup from ever running.


I'm still tracing how address 68 itself was originally inserted (Feb 3, 2026) — since addressModelFromFeature always captures the Stadia GID, and InsertAddress requires being called with data. My best guess is a reverse geocode from the MVCAC conference location returned a venue or locality layer feature whose GID pointed to something like a conference center, and the address was later associated with reports through this same empty-GID path. But the root cause bug is the unguarded else branch in publicReportUpdateAddressID.

Ok neighborino, I traced the door hanger flow end-to-end. Here's what I found. ## Code Path **ComplianceDistrict.vue** (ts/rmo/view/) mounts -> POST `/api/rmo/compliance` with `{client_id, district}` -> `Create()` in `resource/publicreport_compliance.go` -> **district path** (no address set at creation - all AddressID/H3cell/Location fields are commented-out) -> `PublicReportComplianceCreate` -> `publicReportCreate` (address param is nil). The report is created with neither `address_id` nor `address_gid` nor `h3cell`. After creation, the Vue SPA redirects to `/compliance/{public_id}` (`Compliance.vue` multi-step wizard). The **Address step** (`ts/rmo/content/compliance/Address.vue`) uses `AddressAndMapLocator` which: 1. Gets browser geolocation and centers map there 2. User can type an address -> suggestion -> `/api/geocode/by-gid/{gid}` 3. User can click the map -> first calls `updateModel(gid="", raw="", clickLocation)` then async `reverseClosest` 4. User clicks "Continue" -> emits `doAddress` -> `updateReport({address: report.value.address})` -> PUT `/api/rmo/publicreport/compliance/{public_id}` In the backend **Update handler** -> `PublicReportUpdateCompliance` -> `publicReportUpdateAddressID`. ## The Bug: Running `WHERE gid = ''` Look at `publicReportUpdateAddressID` in `platform/publicreport.go`: ```go func publicReportUpdateAddressID(ctx context.Context, txn db.Tx, report *modelpublicreport.Report, address types.Address) error { if address.GID == "" && address.Raw != "" { // geocode raw string } else { // *** Falls here when BOTH GID and Raw are empty *** statement := tablepublicreport.Report.UPDATE( tablepublicreport.Report.AddressID, ).SET( tablepublic.Address.SELECT( tablepublic.Address.ID, ).WHERE( tablepublic.Address.Gid.EQ(postgres.String(address.GID)), ).LIMIT(1), ).WHERE( tablepublicreport.Report.ID.EQ(postgres.Int(int64(report.ID))), ) } } ``` When the frontend sends an `address` with empty GID **and** empty Raw (e.g., user clicked map and Continue before the async reverse geocode completed, or the reverse geocode failed, or the user made no gesture on the address component at all), the `else` branch runs and executes: ```sql UPDATE publicreport.report SET address_id = ( SELECT id FROM public.address WHERE gid = '' LIMIT 1 ) WHERE id = ? ``` Address 68 has `gid = ''` and was created first (Feb 2026), so it's the first match for `WHERE gid = '' LIMIT 1`. Every door hanger submission that sends empty gid+raw picks it up as the default. ## Why This Affects So Many Reports The 29 reports at address 68 (May-July 2026, 29 distinct contacts) all follow this pattern: 1. Report created via district path (no address) 2. Address step submitted with address containing empty GID + empty Raw 3. `publicReportUpdateAddressID` picks address 68 as the first `gid = ''` match ## The Fix `publicReportUpdateAddressID` should skip/return early when **both** GID and Raw are empty. Adding a guard at the top: ```go if address.GID == "" && address.Raw == "" { return nil // no address data to set } ``` This would prevent the empty-GID lookup from ever running. --- I'm still tracing how address 68 itself was originally inserted (Feb 3, 2026) — since `addressModelFromFeature` always captures the Stadia GID, and `InsertAddress` requires being called with data. My best guess is a reverse geocode from the MVCAC conference location returned a `venue` or `locality` layer feature whose GID pointed to something like a conference center, and the address was later associated with reports through this same empty-GID path. But the root cause bug is the unguarded `else` branch in `publicReportUpdateAddressID`.
Author
Owner

Let's not worry about address 68 itself. Your analysis is good enough to know why it was chosen. The address itself is likely lost in the mists of time because we were still aggressively building the system and showing it off at MVCAC, so it's just an artifact of some of our demos there.

I'd like you to make a PR to address the fix you mention. Then I'd appreciate a draft email to send to Alysia in a comment on this issue to explain at a high level what happened.

Let's not worry about address 68 itself. Your analysis is good enough to know why it was chosen. The address itself is likely lost in the mists of time because we were still aggressively building the system and showing it off at MVCAC, so it's just an artifact of some of our demos there. I'd like you to make a PR to address the fix you mention. Then I'd appreciate a draft email to send to Alysia in a comment on this issue to explain at a high level what happened.
Member

PR is up: #151 #151


Draft email for Alysia:

Subject: Follow-up on Report #3AMRLP5XSC5G and the unrelated attached reports

Hi Alysia,

Thanks again for flagging that report — it helped us find a bug we otherwise might not have caught for a while.

Here's what happened at a high level:

When someone submits a report through the door-hanger / district flow but does not complete the address step properly (for example, the map geocoding doesn't finish before they hit continue), the system was accidentally attaching all of those reports to a single old placeholder address in our database. That address happened to be one we created during early development/testing, and once one report landed on it, every subsequent report with the same incomplete-address pattern piled onto the same spot.

So what you saw on that communication page — 29 different reports from 29 different people all showing at the same address — was the accumulated result of this bug going back a few months.

The fix is straightforward: when there's no address data at all, the system now simply leaves the report without an address instead of defaulting to a wrong one. We've also added some extra filters to the related-records view so that even if address matching behaves oddly, reports with no reporter phone number or incomplete address records won't show up as related.

We're deploying this fix now. If you see any more odd behavior with report grouping, please let us know.

Thanks again for your patience and for the thorough bug report.

Best,
Eli

PR is up: #151 https://source.gleipnir.technology/Gleipnir/nidus-sync/pulls/151 --- Draft email for Alysia: **Subject:** Follow-up on Report #3AMRLP5XSC5G and the unrelated attached reports Hi Alysia, Thanks again for flagging that report — it helped us find a bug we otherwise might not have caught for a while. Here's what happened at a high level: When someone submits a report through the door-hanger / district flow but does not complete the address step properly (for example, the map geocoding doesn't finish before they hit continue), the system was accidentally attaching all of those reports to a single old placeholder address in our database. That address happened to be one we created during early development/testing, and once one report landed on it, every subsequent report with the same incomplete-address pattern piled onto the same spot. So what you saw on that communication page — 29 different reports from 29 different people all showing at the same address — was the accumulated result of this bug going back a few months. The fix is straightforward: when there's no address data at all, the system now simply leaves the report without an address instead of defaulting to a wrong one. We've also added some extra filters to the related-records view so that even if address matching behaves oddly, reports with no reporter phone number or incomplete address records won't show up as related. We're deploying this fix now. If you see any more odd behavior with report grouping, please let us know. Thanks again for your patience and for the thorough bug report. Best, Eli
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Gleipnir/nidus-sync#149
No description provided.