# Contact search proposals Situations `redmine_contacts.py search` does not cover, as of the current CLI. Do not treat these commands as existing. The working commands are in [redmine_contacts_search.md](redmine_contacts_search.md). None of these require changing ticket search. They belong on the contacts CLI, with a dry-run default and no write path. ## 1. Exact company or email domain **Need:** "everyone at gatsbylady.com" or "company is UPS" without fuzzy misses and without a short string scoring unrelated names. **Today:** company and email are haystack fields. `search gatsbylady.com` works when that string is inside an email. `search UPS` also fuzzy-matches other fields above `0.68`. There is no `--company` or `--domain`. **Proposal:** ```bash ./redmine_contacts.py --base-url https://rm.ldrprep.com \ search --domain gatsbylady.com --refresh ./redmine_contacts.py --base-url https://rm.ldrprep.com \ search --company UPS --company-exact --refresh ``` `--domain` matches the part after `@`, case-insensitive, across `emails[]`. `--company-exact` matches `company` after trim and case-fold, not a substring of the whole record. If either flag is set, the positional query becomes optional. Print the same text line `search` prints now, plus `--json` from proposal 5. ## 2. List by filter, with no fuzzy query **Need:** every contact with no tags, or every contact updated since a date, or every `customer` tag, without inventing a query that happens to score. **Today:** `search` always requires `query`, then drops anyone under `--min-score`. `--no-tags` and the date flags only narrow that fuzzy set. `fetch --no-tags` caches matches and prints a count, not rows. **Proposal:** a `list` subcommand that reuses `filter_contacts` and does not call `search_contacts`. ```bash ./redmine_contacts.py --base-url https://rm.ldrprep.com \ list --no-tags --updated-after 2026-09-01 --refresh --limit 50 ./redmine_contacts.py --base-url https://rm.ldrprep.com \ list --with-tag promotional-spam --refresh ``` Require at least one filter, same guard as `batch-update`, so `list` cannot dump the whole CRM by accident. Default `--limit 50`. Order by `updated_on` descending. Text lines should include id, name, email, and `tag_list`, because a bare fuzzy score is meaningless here. Do not overload `search` by making `query` optional. Operators already have to know that `search` is fuzzy. ## 3. Tagged customers with an empty Account Number **Need:** billed or tagged customers who have no NMI account id in custom field 70 (`Account Number`), so the jumpstart can see who is unmatched. **Today:** a known account number can be fuzzy-searched if it is already stored. There is no empty-field filter. `show` is one id at a time. **Proposal:** ```bash ./redmine_contacts.py --base-url https://rm.ldrprep.com \ list --with-tag customer --missing-custom-field account-number --refresh ``` Resolve `account-number` with the existing custom-field slug (`slugify_custom_field_name`). Missing means the field is absent, null, or whitespace. Do not treat `"0"` as empty. Read-only. This is the list command from proposal 2 plus one predicate, not a new writer. ## 4. Open tickets from contacts with a tag **Need:** New Customer Service tickets whose sender is tagged `promotional-spam`, without hand-joining ids. **Today:** ticket JSON does not store `tag_list`. `bin/rtl search --where 'helpdesk_ticket.contact.tag_list.includes(...)'` matches nothing. The working path is tag search, then `helpdesk_ticket.contact_id == N` per id. **Proposal:** a read-only join, not a ticket-cache field. ```bash ./redmine_contacts.py --base-url https://rm.ldrprep.com \ tagged-issues --with-tag promotional-spam --issue-status New --open-only --refresh ``` Steps: 1. Resolve contact ids with the list filter from proposal 2, live or `--refresh`. 2. For each id, call the existing `helpdesk-issues` endpoint, or scan the ticket cache for `helpdesk_ticket.contact_id`. 3. Keep issues that are New, open, and in `customer-service`. 4. Print `issue_id contact_id email subject`, and stop after `--limit` issues (default 20). Do not close anything. Do not add `tag_list` to the ticket cache in this change. A stale contacts cache must be called out in the header line (`cache_refreshed=yes|no`). Refuse to run if the contact cache host does not match `--base-url`, which the current cache reader already does. ## 5. Script output: ids and JSON **Need:** feed contact ids into `bin/rtl` or a later join without scraping `score=1.00 via email`. **Today:** `search` prints prose. `show` and `helpdesk-issues` print JSON for one id. There is no `--ids-only` on this CLI. `bin/rtl contacts-search --ids-only` is the other cache. **Proposal:** `--json` and `--ids-only` on `search` and on `list`. ```bash ./redmine_contacts.py --base-url https://rm.ldrprep.com \ search promotional-spam --with-tag promotional-spam --refresh --ids-only ``` `--json` prints one object per line: `id`, `name`, `email`, `tag_list`, and `score` when the command was fuzzy. `--ids-only` prints one integer per line. Neither flag changes which rows match. ## Out of scope Duplicate detection (same email or phone on two contacts) needs a separate scan and a human merge step. Do not fold it into `search`. The existing duplicate-merge note stays the place for that design. Do not make `batch-update` printable as a search. It requires a mutation payload on purpose.