Files
redmine/docs/ticket_spam_cleanup_handoff_2026-07-09.md
T
Jason Thistlethwaite 9ee9eb3ec6 Organized
2026-10-17 20:50:35 -04:00

825 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Ticket-Centric Spam Cleanup Handoff — 2026-07-09
## Purpose
This handoff captures the current state of the new ticket-centric Redmine maintenance substrate and the spam cleanup workflow for the `customer-service` project.
The immediate business goal is to reduce visible spam clutter in `customer-service` without disrupting existing NMI production scripts or doing unsafe bulk Redmine writes.
## Important Safety / Operating Rules
1. **Do not modify NMI scripts right now.** Existing NMI cron scripts are reference material only.
2. **Dry-run first.** Redmine mutations require explicit `--apply`.
3. **Reads do not impersonate.** Ordinary sync/search/dry-run planning should use normal API access.
4. **Writes impersonate `rebot`.** Spam cleanup / ticket maintenance writes should use `X-Redmine-Switch-User: rebot` (Harry Botter), because Redmine activity logs should show automated cleanup under that account, not Jason.
5. **Treat impersonation like sudo.** Use it only where the write is supposed to happen under that agent account.
6. **LAN first.** The validated target so far is the LAN Redmine copy at `http://192.168.50.170`, project `customer-service`.
7. **Confirm status IDs before applying.** Existing evidence suggests `status_id=5` is closed, but verify before first real apply if uncertain.
## Relevant Files
- Tool: `/home/jason/projects/redmine/redmine_tickets.py`
- Tests: `/home/jason/projects/redmine/tests/test_redmine_tickets.py`
- Plan: `/home/jason/projects/redmine/docs/ticket_sync_tool_plan.md`
- General project handoff: `/home/jason/projects/redmine/HANDOFF.md`
- Helpdesk API include docs: `/home/jason/projects/redmine/docs/redmine_issue_api_helpdesk_include.md`
- Existing contact tool: `/home/jason/projects/redmine/redmine_contacts.py`
## Why `redmine_tickets.py` Exists
The earlier `redmine_contacts.py` contact conveyor is useful for contact enrichment and contact-level routing, but it is not the best first tool for clearing visible spam from `customer-service` because the UI pain is ticket-level: the top/open tickets themselves are clutter.
`redmine_tickets.py` is the new ticket-centric substrate. It syncs enriched issue payloads, searches cached tickets, and runs idempotent ticket actions.
## Modern Helpdesk Access
Do **not** recreate RebotSlick `PullHelpdeskInfo` behavior. The old separate helpdesk pull/cache pattern is obsolete because the local Redmine Helpdesk plugin now supports:
```text
GET /issues/:id.json?include=journals,helpdesk
```
This returns normal Redmine issue data plus embedded `helpdesk_ticket` metadata, including contact/email/source fields.
## Current Architecture
### Ticket payload cache
Per-ticket JSON files are stored under a configurable cache directory, e.g.:
```text
.cache/redmine_tickets_lan/customer-service/41788.json
.cache/redmine_tickets_lan/customer-service/41789.json
```
### Ticket/action state
State is currently manifest-backed:
```text
.cache/redmine_tickets_lan/manifest.json
```
But the code now separates payload cache from state storage:
- `TicketCache` handles ticket JSON cache + delegates state operations.
- `ManifestTicketStateStore` is the current state backend.
- The abstraction is intentionally small so SQLite can replace the manifest later for thousands of tickets.
Required state-store methods:
```python
get_ticket_state(issue_id)
upsert_ticket_state(issue_id, fields)
has_action(issue_id, action_name)
record_action(issue_id, action_name, result, metadata=None)
```
## Implemented Commands
### Sync enriched tickets
```bash
cd /home/jason/projects/redmine
./redmine_tickets.py \
--base-url http://192.168.50.170 \
--project customer-service \
--cache-dir .cache/redmine_tickets_lan \
sync \
--status-id open \
--max-tickets 10 \
--order newest-updated-first
```
Behavior:
- fetches issue stubs/pages from Redmine
- fetches full issue payloads with `include=journals,helpdesk`
- writes only missing/updated tickets to cache
- second identical run skips unchanged tickets
Verified output:
```text
Sync complete: seen=10 fetched=10 skipped_cached=0 written=10
Sync complete: seen=10 fetched=0 skipped_cached=10 written=0
```
### Search cached tickets
```bash
./redmine_tickets.py \
--cache-dir .cache/redmine_tickets_lan \
--project customer-service \
search \
--max-tickets 10 \
--order newest-updated-first
```
Validated that this matches the UI top-open-ticket pattern and includes spam-looking tickets such as:
```text
#41788 Up to 20% Off Select Fans & Blowers
#41789 Up to 20% Off Select Fans & Blowers
```
### Spam check cached tickets
```bash
./redmine_tickets.py \
--cache-dir .cache/redmine_tickets_lan \
--project customer-service \
spam-check
```
Validated result:
```text
#41788 SPAM score=2 from=reply@globalindustrial.com Up to 20% Off Select Fans & Blowers
#41789 SPAM score=2 from=reply@globalindustrial.com Up to 20% Off Select Fans & Blowers
```
Record local idempotency only (no Redmine mutation):
```bash
./redmine_tickets.py \
--cache-dir .cache/redmine_tickets_lan \
--project customer-service \
spam-check \
--apply
```
After this, a second `spam-check` checks 0 tickets because `spam-check` action markers are recorded.
### Plan closure for spam tickets
Dry-run:
```bash
./redmine_tickets.py \
--base-url http://192.168.50.170 \
--project customer-service \
--cache-dir .cache/redmine_tickets_lan \
close-if-spam \
--close-status-id 5 \
--contact-ticket-limit 10
```
Validated output:
```text
#41788 close_payload={"notes": "Closed as likely spam by redmine_tickets.py close-if-spam.", "status_id": 5} contact_id=621 contact_spam=4/10 likely_spammer=True Up to 20% Off Select Fans & Blowers
#41789 close_payload={"notes": "Closed as likely spam by redmine_tickets.py close-if-spam.", "status_id": 5} contact_id=621 contact_spam=4/10 likely_spammer=True Up to 20% Off Select Fans & Blowers
Close-if-spam complete: planned=2 apply=False
Dry run only. Re-run with --apply to close Redmine issues and record idempotency markers.
```
No Redmine mutation has been applied yet.
## Actual Closure Command (Pending Approval)
After confirming `status_id=5` is the correct closed status on the LAN copy, this is the command that would close the two planned tickets and record idempotency markers:
```bash
cd /home/jason/projects/redmine
./redmine_tickets.py \
--base-url http://192.168.50.170 \
--project customer-service \
--cache-dir .cache/redmine_tickets_lan \
close-if-spam \
--close-status-id 5 \
--contact-ticket-limit 10 \
--impersonate-user rebot \
--apply
```
Expected write behavior:
- sends `PUT /issues/:id.json`
- payload:
```json
{
"issue": {
"status_id": 5,
"notes": "Closed as likely spam by redmine_tickets.py close-if-spam."
}
}
```
- includes header:
```text
X-Redmine-Switch-User: rebot
```
- records local actions:
- `close-if-spam = closed`
- `contact-spam-review = likely-spammer` or `single-ticket-spam`
## Tests / Verification
Current test command:
```bash
python3 -m unittest discover -s tests -p 'test_redmine_tickets.py' -v
```
Last verified:
```text
Ran 11 tests
OK
```
Compile command:
```bash
python3 -m py_compile redmine_tickets.py tests/test_redmine_tickets.py redmine_contacts.py
```
Last verified: OK.
## 25-Ticket LAN Evaluation — 2026-07-09
Jason confirmed `status_id=5` is Closed. A fresh throwaway cache was used so old action markers would not hide results:
```text
.cache/redmine_tickets_lan_25_eval
```
Commands run:
```bash
./redmine_tickets.py \
--base-url http://192.168.50.170 \
--project customer-service \
--cache-dir .cache/redmine_tickets_lan_25_eval \
sync \
--status-id open \
--max-tickets 25 \
--order newest-updated-first
./redmine_tickets.py \
--cache-dir .cache/redmine_tickets_lan_25_eval \
--project customer-service \
spam-check \
--apply
./redmine_tickets.py \
--base-url http://192.168.50.170 \
--project customer-service \
--cache-dir .cache/redmine_tickets_lan_25_eval \
close-if-spam \
--close-status-id 5 \
--contact-ticket-limit 10
```
Verified output summary:
```text
Sync complete: seen=25 fetched=25 skipped_cached=0 written=25
Spam check complete: checked=25 apply=True
Close-if-spam complete: planned=5 apply=False
SUMMARY cached_tickets=25
SUMMARY spam=5 not_spam=20
```
Closure candidates produced by dry-run:
```text
#41753 Automated unfulfillable FBA inventory removal notification — from fba-noreply@amazon.com — contact_id=51 — contact_spam=10/10 — likely_spammer=True
#41760 Celebrate America's 250th: Save Up To 40% Off — from reply@globalindustrial.com — contact_id=621 — contact_spam=4/10 — likely_spammer=True
#41761 Celebrate America's 250th: Save Up To 40% Off — from reply@globalindustrial.com — contact_id=621 — contact_spam=4/10 — likely_spammer=True
#41767 — — from enews-noreply@qnap.com — contact_id=3162 — contact_spam=10/10 — likely_spammer=True
#41776 Your favorite sale ENDS THURSDAY ⏳ — from noreply@mail.vistaprint.com — contact_id=3795 — contact_spam=10/10 — likely_spammer=True
```
Important finding from first run: the scorer conflated **spam/promotional clutter** with **automated operational notifications**. Example: `#41753` from `fba-noreply@amazon.com` is likely an automated Amazon/FBA operational notification, not normal spam.
No Redmine close mutation was applied during this 25-ticket evaluation.
## Refined Promotional-Junk Evaluation — 2026-07-09
The scorer was refined to target Jason's immediate goal: quickly and reliably close **obvious unsolicited promotional/sales junk**, such as Walmart/Uline/GlobalIndustrial/Vistaprint/QNAP-style promotional mail and lead-gen/sales outreach from people trying to sell LDR something without an established relationship.
False-positive protections added:
- `automated-operational`: Amazon/FBA/order/API/payment-style notifications are excluded from the default auto-close class.
- Customer/vendor operational threads are protected from staff-reply contamination: LDR staff-authored journal notes are no longer scored as spam evidence.
- Standalone `SEO` in customer operational context is not treated as a sales trigger.
Regression tests now cover:
- promotional sale email => `spam-promotional`
- cleaning-service sales pitch => `spam-promotional`
- Amazon/FBA notification => `automated-operational`, not spam
- real FBA returns inquiry with LDR pricing reply => not spam
- customer operational context mentioning SEO => not spam
Fresh final cache:
```text
.cache/redmine_tickets_lan_25_promotional_eval_final_1783600119
```
Final verified summary:
```text
Sync complete: seen=25 fetched=25 skipped_cached=0 written=25
Close-if-spam complete: planned=6 apply=False
SUMMARY categories={'unknown-review': 17, 'automated-operational': 2, 'spam-promotional': 6}
```
Current safe close candidates from the final dry-run:
```text
#41767 — from enews-noreply@qnap.com — QNAP newsletter/promo
#41776 Your favorite sale ENDS THURSDAY ⏳ — from noreply@mail.vistaprint.com
#41788 Up to 20% Off Select Fans & Blowers — from reply@globalindustrial.com
#41789 Up to 20% Off Select Fans & Blowers — from reply@globalindustrial.com
#41804 Do you serve e-commerce brands? — from d.elliott@fulfillmentviewpro.com — 3PL lead-gen/sales pitch
#41834 Japan’s “JC-STAR Level 1” — from enews-noreply@qnap.com — QNAP promotional/newsletter item
```
No Redmine close mutation was applied during this final evaluation.
### LAN apply verification — 2026-07-09
Jason approved moving toward closing the obvious junk. The following command was run against the final 25-ticket cache:
```bash
./redmine_tickets.py \
--base-url http://192.168.50.170 \
--project customer-service \
--cache-dir .cache/redmine_tickets_lan_25_promotional_eval_final_1783600119 \
close-if-spam \
--close-status-id 5 \
--contact-ticket-limit 10 \
--impersonate-user rebot \
--apply
```
Applied closures:
```text
#41767
#41776
#41788
#41789
#41804
#41834
```
Idempotency check:
```text
Close-if-spam complete: planned=0 apply=True
```
API verification after apply:
```text
41767 status=Closed close_note_author=Harry B. close_note_found=True
41776 status=Closed close_note_author=Harry B. close_note_found=True
41788 status=Closed close_note_author=Harry B. close_note_found=True
41789 status=Closed close_note_author=Harry B. close_note_found=True
41804 status=Closed close_note_author=Harry B. close_note_found=True
41834 status=Closed close_note_author=Harry B. close_note_found=True
```
## Current Limitations / Known Next Improvements
1. `manifest.json` is acceptable for the current tracer, but SQLite should be considered once caches/actions cover thousands of tickets.
2. Spam scoring is still intentionally simple and reviewable. It now distinguishes `spam-promotional`, `automated-operational`, and `unknown-review`; future classes can be added as patterns emerge.
3. Contact-level spammer handling is only advisory right now. The tool reports `likely_spammer=True`, but does not yet tag the contact.
4. First real close apply for each newly expanded batch should happen on LAN only, then immediately re-sync/verify status and activity author.
5. The production Redmine path should remain out of scope until LAN write behavior is proven and reviewed.
## 50-Ticket Refined Promotional-Junk Dry Run — 2026-07-09
Fresh cache:
```text
.cache/redmine_tickets_lan_50_promotional_eval_refined_1783600628
```
Verified summary:
```text
Sync complete: seen=50 fetched=50 skipped_cached=0 written=50
Close-if-spam complete: planned=9 apply=False
SUMMARY categories={'unknown-review': 32, 'spam-promotional': 9, 'automated-operational': 9}
```
Promotional close candidates:
```text
#41685 Samsung promo — noreply_samsung@sea.samsung.com — "Jason, we’ve missed you. We just can't stop innovating."
#41687 QNAP promo — enews-noreply@qnap.com — "Qsync Now Supports File Backup on Mac"
#41698 Vistaprint promo — noreply@mail.vistaprint.com — "Life’s best moments, treasured forever"
#41705 Popmenu/restaurant promo — noreply@popmenu.com — "Take a break from cooking!"
#41722 3PL lead-gen/sales pitch — hello@offer.search3pl.com — "Live fulfilment leads waiting for you to connect"
#41738 Vistaprint promo — noreply@mail.vistaprint.com — "Up to 15% off top business products that’ll draw them in"
#41746 Business Journals events/nominations promo — replycincinnati@engaged.bizjournals.com — "Hi LDR, here are our upcoming events and nominations"
#41760 Global Industrial sale — reply@globalindustrial.com — "Celebrate America's 250th: Save Up To 40% Off"
#41761 Global Industrial sale — reply@globalindustrial.com — "Celebrate America's 250th: Save Up To 40% Off"
```
Operational/system notifications excluded from auto-close:
```text
#41694 USPS eReceipt
#41704 2024 UNPAID INVOICES $3,413.55
#41714 Undelivered Mail Returned to Sender
#41716 Critical security alert
#41726 UPS Invoice Now Available
#41753 Automated unfulfillable FBA inventory removal notification
#41765 Amazon Listings API changes
#41772 Cintas Payment Status
#41823 Amazon order confirmation
```
No Redmine mutation was applied during this 50-ticket dry run.
### 50-ticket LAN apply verification — 2026-07-09
Jason approved applying the 9-candidate promotional set. The following command was run against the exact reviewed cache:
```bash
./redmine_tickets.py \
--base-url http://192.168.50.170 \
--project customer-service \
--cache-dir .cache/redmine_tickets_lan_50_promotional_eval_refined_1783600628 \
close-if-spam \
--close-status-id 5 \
--contact-ticket-limit 10 \
--impersonate-user rebot \
--apply
```
Applied closures:
```text
#41685
#41687
#41698
#41705
#41722
#41738
#41746
#41760
#41761
```
Idempotency check:
```text
Close-if-spam complete: planned=0 apply=True
```
API verification after apply:
```text
41685 status=Closed close_note_author=Harry B. close_note_found=True
41687 status=Closed close_note_author=Harry B. close_note_found=True
41698 status=Closed close_note_author=Harry B. close_note_found=True
41705 status=Closed close_note_author=Harry B. close_note_found=True
41722 status=Closed close_note_author=Harry B. close_note_found=True
41738 status=Closed close_note_author=Harry B. close_note_found=True
41746 status=Closed close_note_author=Harry B. close_note_found=True
41760 status=Closed close_note_author=Harry B. close_note_found=True
41761 status=Closed close_note_author=Harry B. close_note_found=True
```
## Recommended Next Session Steps
1. If Jason approves the current 27-candidate production 200-ticket set, apply closures on production using `--impersonate-user rebot` and the exact cache `.cache/redmine_tickets_prod_200_promotional_eval_1783612885`.
2. Optionally apply the 3 `maybe-customer` contact tags from the same cache.
3. Re-sync/inspect via Redmine API/UI and verify closed status, close-note author `Harry B.`, second-apply idempotency, and local action markers.
4. Continue with another bounded production dry-run batch.
## Customer identification update — 2026-07-09
Jason confirmed the following contacts/patterns are customers and should be tagged `Customer`:
```text
koki-takeda@shinkiya-inc.com
hello@bohoflow.co
hi5@bavariatrachten.com
maia@solride.org
mike@solride.org
info@u2wear.com
sales@u2wear.com
any Cyberstorm email
Esire-family emails
```
Important correction: do **not** treat incidental substrings such as `desiree` in Indeed/applicant email aliases as Esire-family customer matches.
Production contact tagging was applied with `impersonate_user='rebot'`:
```text
MATCHED_COUNT 31
UPDATED_COUNT 25
SKIPPED_ALREADY_CUSTOMER 6
```
Two accidental applicant matches were corrected immediately:
```text
4253 conversation-desireethrasher-customerreturnsinspect-0n3@indeedemail.com: removed Customer, left applicant
3934 conversation-desireethrasher-customerreturnsinspect-9nv@indeedemail.com: removed Customer, left applicant
```
After customer tagging, re-running the production 200 close plan still produced 27 candidates and kept `#41238` removed from the plan.
## Customer-tag safety gate — 2026-07-09
New hard rule: before closing any ticket as spam, `redmine_tickets.py` fetches the Helpdesk contact and checks `tag_list`. If the contact has tag `customer`, the ticket is **not** closed as spam. Absence of the `customer` tag does **not** imply spam.
Jason also clarified the closure risk model:
- Closed tickets are not deleted.
- If the other party replies to a closed Helpdesk ticket, Redmine reopens it as `New Message`.
- Therefore, stale 30+ day tickets that match a verified `spam-promotional` profile are reasonable fast-close candidates once contact safety gates pass.
- However, if a ticket was previously closed and then reopened because the contact messaged again, do **not** auto-close it again. Escalate instead: bump priority and optionally set a due date 1–3 days out.
A separate action now exists to tag contacts needing manual/customer review:
```bash
./redmine_tickets.py \
--base-url https://rm.ldrprep.com \
--project customer-service \
--cache-dir <cache> \
tag-maybe-customer [--apply]
```
This adds `maybe-customer` to contacts with inbound/established-business inquiry evidence, using `--impersonate-user rebot` for writes.
Tests now cover 20 cases, including the customer-tag skip and `maybe-customer` tag action.
## Production 200-Ticket Dry Run With Customer Gate — 2026-07-09
Production cache:
```text
.cache/redmine_tickets_prod_200_promotional_eval_1783612885
```
Re-running the close plan after adding the contact `customer` safety gate produced:
```text
Close-if-spam complete: planned=27 apply=False
```
The customer-tag gate removed `#41238` from the close plan, which was the suspicious `manager@sonisha.com` / `Account Queries` candidate.
Current 27 close candidates:
```text
#41098
#41114
#41121
#41122
#41127
#41128
#41153
#41164
#41167
#41168
#41201
#41222
#41226
#41227
#41242
#41248
#41269
#41293
#41297
#41315
#41331
#41340
#41341
#41347
#41368
#41373
#41850
```
Maybe-customer dry-run produced 3 contact tag candidates:
```text
contact_id=4470 source_issue_id=41424 tag_list=["maybe-customer"]
contact_id=3919 source_issue_id=41448 tag_list=["maybe-customer"]
contact_id=4476 source_issue_id=41504 tag_list=["maybe-customer"]
```
No production mutation was applied after adding this guard.
## Production 100-Ticket Promotional-Junk Dry Run — 2026-07-09
Production URL:
```text
https://rm.ldrprep.com
```
Fresh cache:
```text
.cache/redmine_tickets_prod_100_promotional_eval_1783609390
```
Verified summary:
```text
Sync complete: seen=100 fetched=100 skipped_cached=0 written=100
Close-if-spam complete: planned=20 apply=False
SUMMARY categories={'unknown-review': 59, 'automated-operational': 21, 'spam-promotional': 20}
```
Promotional close candidates:
```text
#41397 Business Journals events/nominations promo — replycincinnati@engaged.bizjournals.com
#41430 Vistaprint sale — noreply@mail.vistaprint.com
#41431 Business Summit promo — replycincinnati@partner.bizjournals.com
#41657 Global Industrial Kee Safety promo — reply@globalindustrial.com
#41658 Global Industrial Kee Safety promo — reply@globalindustrial.com
#41685 Samsung promo — noreply_samsung@sea.samsung.com
#41687 QNAP promo — enews-noreply@qnap.com
#41698 Vistaprint promo — noreply@mail.vistaprint.com
#41705 Popmenu restaurant promo — noreply@popmenu.com
#41722 3PL lead-gen sales pitch — hello@offer.search3pl.com
#41738 Vistaprint promo — noreply@mail.vistaprint.com
#41746 Business Journals events/nominations promo — replycincinnati@engaged.bizjournals.com
#41760 Global Industrial sale — reply@globalindustrial.com
#41761 Global Industrial sale — reply@globalindustrial.com
#41767 QNAP newsletter/promo — enews-noreply@qnap.com
#41776 Vistaprint sale — noreply@mail.vistaprint.com
#41788 Global Industrial sale — reply@globalindustrial.com
#41789 Global Industrial sale — reply@globalindustrial.com
#41804 3PL lead-gen sales pitch — d.elliott@fulfillmentviewpro.com
#41834 QNAP promotional/newsletter item — enews-noreply@qnap.com
```
No production mutation was applied during this dry run.
### Production apply verification — 2026-07-09
Jason approved applying the 20-candidate promotional set. The following command was run against the exact reviewed production cache:
```bash
./redmine_tickets.py \
--base-url https://rm.ldrprep.com \
--project customer-service \
--cache-dir .cache/redmine_tickets_prod_100_promotional_eval_1783609390 \
close-if-spam \
--close-status-id 5 \
--contact-ticket-limit 10 \
--impersonate-user rebot \
--apply
```
Applied closures:
```text
#41397
#41430
#41431
#41657
#41658
#41685
#41687
#41698
#41705
#41722
#41738
#41746
#41760
#41761
#41767
#41776
#41788
#41789
#41804
#41834
```
Idempotency check:
```text
Close-if-spam complete: planned=0 apply=True
```
API verification after apply:
```text
41397 status=Closed close_note_author=Harry B. close_note_found=True
41430 status=Closed close_note_author=Harry B. close_note_found=True
41431 status=Closed close_note_author=Harry B. close_note_found=True
41657 status=Closed close_note_author=Harry B. close_note_found=True
41658 status=Closed close_note_author=Harry B. close_note_found=True
41685 status=Closed close_note_author=Harry B. close_note_found=True
41687 status=Closed close_note_author=Harry B. close_note_found=True
41698 status=Closed close_note_author=Harry B. close_note_found=True
41705 status=Closed close_note_author=Harry B. close_note_found=True
41722 status=Closed close_note_author=Harry B. close_note_found=True
41738 status=Closed close_note_author=Harry B. close_note_found=True
41746 status=Closed close_note_author=Harry B. close_note_found=True
41760 status=Closed close_note_author=Harry B. close_note_found=True
41761 status=Closed close_note_author=Harry B. close_note_found=True
41767 status=Closed close_note_author=Harry B. close_note_found=True
41776 status=Closed close_note_author=Harry B. close_note_found=True
41788 status=Closed close_note_author=Harry B. close_note_found=True
41789 status=Closed close_note_author=Harry B. close_note_found=True
41804 status=Closed close_note_author=Harry B. close_note_found=True
41834 status=Closed close_note_author=Harry B. close_note_found=True
```
## 100-Ticket Refined Promotional-Junk Dry Run — 2026-07-09
Fresh cache:
```text
.cache/redmine_tickets_lan_100_promotional_eval_refined_1783601115
```
Verified summary:
```text
Sync complete: seen=100 fetched=100 skipped_cached=0 written=100
Close-if-spam complete: planned=9 apply=False
SUMMARY categories={'unknown-review': 68, 'spam-promotional': 9, 'automated-operational': 23}
```
Promotional close candidates:
```text
#41227 Global Industrial clearance — reply@globalindustrial.com — "Clearance: Bestsellers At Best Prices"
#41297 Vistaprint sale — noreply@mail.vistaprint.com — "Up to 30% off select customer faves, like mugs, postcards & more"
#41340 Global Industrial AC/fans — reply@globalindustrial.com — "Advanced AC, Wall Fans, And Cooling Units"
#41373 TikTok Shop promo — taylor@shop.tiktok.com — "The simplest way to start selling this June"
#41397 Business Journals events/nominations promo — replycincinnati@engaged.bizjournals.com — "Hi LDR, here are our upcoming events and nominations"
#41430 Vistaprint sale — noreply@mail.vistaprint.com — "Email exclusive! Up to 40% off select top products ENDS TOMORROW"
#41431 Business Summit promo — replycincinnati@partner.bizjournals.com — "Regional Business Summit Returns August 5"
#41657 Global Industrial Kee Safety promo — reply@globalindustrial.com — "'Kee Safety' Modular & Durable Fall Prevention Equipment"
#41658 Global Industrial Kee Safety promo — reply@globalindustrial.com — "'Kee Safety' Modular & Durable Fall Prevention Equipment"
```
False-positive protections added/verified during this 100-ticket run:
- VoIP voicemail / call transcription tickets are `automated-operational`, not promotional spam.
- Inbound buying/relationship inquiries like RePackify Gaylord purchase interest and u2wear returns-provider follow-up remain `unknown-review`, not auto-close.
- Receipts, bounces, security alerts, invoices, Amazon/FBA/order notices remain `automated-operational`, not auto-close.
No Redmine mutation was applied during this 100-ticket dry run.
### 100-ticket LAN apply verification — 2026-07-09
Jason approved applying the 9-candidate promotional set. The following command was run against the exact reviewed cache:
```bash
./redmine_tickets.py \
--base-url http://192.168.50.170 \
--project customer-service \
--cache-dir .cache/redmine_tickets_lan_100_promotional_eval_refined_1783601115 \
close-if-spam \
--close-status-id 5 \
--contact-ticket-limit 10 \
--impersonate-user rebot \
--apply
```
Applied closures:
```text
#41227
#41297
#41340
#41373
#41397
#41430
#41431
#41657
#41658
```
Idempotency check:
```text
Close-if-spam complete: planned=0 apply=True
```
API verification after apply:
```text
41227 status=Closed close_note_author=Harry B. close_note_found=True
41297 status=Closed close_note_author=Harry B. close_note_found=True
41340 status=Closed close_note_author=Harry B. close_note_found=True
41373 status=Closed close_note_author=Harry B. close_note_found=True
41397 status=Closed close_note_author=Harry B. close_note_found=True
41430 status=Closed close_note_author=Harry B. close_note_found=True
41431 status=Closed close_note_author=Harry B. close_note_found=True
41657 status=Closed close_note_author=Harry B. close_note_found=True
41658 status=Closed close_note_author=Harry B. close_note_found=True
```