# 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 \ 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 ```