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

27 KiB
Raw Permalink Blame History

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:

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.:

.cache/redmine_tickets_lan/customer-service/41788.json
.cache/redmine_tickets_lan/customer-service/41789.json

Ticket/action state

State is currently manifest-backed:

.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:

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

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:

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

./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:

#41788 Up to 20% Off Select Fans & Blowers
#41789 Up to 20% Off Select Fans & Blowers

Spam check cached tickets

./redmine_tickets.py \
  --cache-dir .cache/redmine_tickets_lan \
  --project customer-service \
  spam-check

Validated result:

#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):

./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:

./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:

#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:

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:
    {
      "issue": {
        "status_id": 5,
        "notes": "Closed as likely spam by redmine_tickets.py close-if-spam."
      }
    }
    
  • includes header:
    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:

python3 -m unittest discover -s tests -p 'test_redmine_tickets.py' -v

Last verified:

Ran 11 tests
OK

Compile command:

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:

.cache/redmine_tickets_lan_25_eval

Commands run:

./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:

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:

#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:

.cache/redmine_tickets_lan_25_promotional_eval_final_1783600119

Final verified summary:

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:

#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:

./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:

#41767
#41776
#41788
#41789
#41804
#41834

Idempotency check:

Close-if-spam complete: planned=0 apply=True

API verification after apply:

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:

.cache/redmine_tickets_lan_50_promotional_eval_refined_1783600628

Verified summary:

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:

#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:

#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:

./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:

#41685
#41687
#41698
#41705
#41722
#41738
#41746
#41760
#41761

Idempotency check:

Close-if-spam complete: planned=0 apply=True

API verification after apply:

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
  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:

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':

MATCHED_COUNT 31
UPDATED_COUNT 25
SKIPPED_ALREADY_CUSTOMER 6

Two accidental applicant matches were corrected immediately:

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:

./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:

.cache/redmine_tickets_prod_200_promotional_eval_1783612885

Re-running the close plan after adding the contact customer safety gate produced:

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:

#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:

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:

https://rm.ldrprep.com

Fresh cache:

.cache/redmine_tickets_prod_100_promotional_eval_1783609390

Verified summary:

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:

#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:

./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:

#41397
#41430
#41431
#41657
#41658
#41685
#41687
#41698
#41705
#41722
#41738
#41746
#41760
#41761
#41767
#41776
#41788
#41789
#41804
#41834

Idempotency check:

Close-if-spam complete: planned=0 apply=True

API verification after apply:

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:

.cache/redmine_tickets_lan_100_promotional_eval_refined_1783601115

Verified summary:

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:

#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:

./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:

#41227
#41297
#41340
#41373
#41397
#41430
#41431
#41657
#41658

Idempotency check:

Close-if-spam complete: planned=0 apply=True

API verification after apply:

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