fix(po-email): a dry run must not consume a message

The state file was written before the documents were classified, so a --dry-run
marked every message as seen. A new PO seen first by a dry run would then be
skipped by every later real run -- never filed, never flagged for review.

Now a message is remembered only after it has been classified, and only on a real
run; the outcome (filed, already_filed, duplicate_number, needs_review) is stored
with it. A dry run leaves the state file untouched, and the report now says how
many messages were already seen so an idle hour is not mistaken for a failure.
This commit is contained in:
DEls 2026-11-02 23:16:28 +02:00
parent 607de20232
commit de8aa83ef1
2 changed files with 36 additions and 6 deletions

View file

@ -44,6 +44,11 @@ python3 scripts/po_email_intake.py --force # re-check every inbox message
It needs the Mac to be awake and Mail.app reachable, and it needs the **Automation** permission
for Mail (granted once — System Settings → Privacy & Security → Automation).
A `--dry-run` never consumes a message: it reports what it would do and leaves the state file
untouched, so a PO first seen by a dry run is still filed by the next real run. A message is
remembered only after it has been classified, together with what happened to it
(`already_filed`, `duplicate_number`, `filed`, `needs_review`).
> The n8n workflow below does the same job server-side, with no dependency on this Mac. The
> script is the route that works today, while the workflow waits on its Microsoft credential.

View file

@ -340,6 +340,7 @@ def main() -> int:
"ran_at": now_iso(),
"dry_run": args.dry_run,
"messages_seen": len(messages),
"already_seen": [],
"filed": [],
"already_filed": [],
"duplicate_number": [],
@ -349,6 +350,7 @@ def main() -> int:
for msg in messages:
if not args.force and msg["id"] in state["seen"]:
report["already_seen"].append(msg["subject"])
continue
try:
source = raw_source(msg["id"])
@ -358,14 +360,21 @@ def main() -> int:
message_id = header_of(source, "Message-ID")
attachments = pdf_attachments(source)
state["seen"][msg["id"]] = {
"message_id": message_id,
"subject": msg["subject"],
"seen_at": now_iso(),
}
# The message is only remembered once it has been classified. A dry run must
# not consume it -- otherwise a new PO seen first by --dry-run would be
# skipped by every later real run and never filed, never flagged.
outcomes: list[str] = []
if not attachments:
outcomes.append("no_pdf")
report["skipped"].append({"subject": msg["subject"], "reason": "no PDF attachment"})
if not args.dry_run:
state["seen"][msg["id"]] = {
"message_id": message_id,
"subject": msg["subject"],
"seen_at": now_iso(),
"outcome": outcomes,
}
continue
for filename, blob in attachments:
@ -381,6 +390,7 @@ def main() -> int:
"reason": "no PO number found in the document",
}
)
outcomes.append("needs_review")
continue
# 1. same document, same site -> already filed
@ -389,6 +399,7 @@ def main() -> int:
report["already_filed"].append(
{"po_number": po_number, "subject": msg["subject"], "file": filename}
)
outcomes.append("already_filed")
continue
# 2. same PO number already in the system -> a human must look
@ -403,6 +414,7 @@ def main() -> int:
"reason": "PO number already in the system",
}
)
outcomes.append("duplicate_number")
continue
site = resolve_site(parsed["subject_line"], sites) or resolve_site(
@ -417,6 +429,7 @@ def main() -> int:
"reason": "no MEGA site found in the PO subject line or the email",
}
)
outcomes.append("needs_review")
continue
meta = {
@ -443,6 +456,7 @@ def main() -> int:
"dry_run": True,
}
)
outcomes.append("would_file")
continue
created = pb_create_po(token, blob, site["id"], file_hash, meta)
@ -456,9 +470,19 @@ def main() -> int:
"file": filename,
}
)
outcomes.append("filed")
mark_handled(msg["id"])
STATE_FILE.write_text(json.dumps(state, indent=1))
if not args.dry_run:
state["seen"][msg["id"]] = {
"message_id": message_id,
"subject": msg["subject"],
"seen_at": now_iso(),
"outcome": outcomes,
}
if not args.dry_run:
STATE_FILE.write_text(json.dumps(state, indent=1))
LAST_RUN_FILE.write_text(json.dumps(report, indent=1))
with RUN_LOG.open("a") as fh:
fh.write(json.dumps({k: v for k, v in report.items() if k != "skipped"}) + "\n")
@ -466,6 +490,7 @@ def main() -> int:
filed = len(report["filed"])
print(
f"PO intake: {report['messages_seen']} message(s) in inbox | "
f"{len(report['already_seen'])} already seen | "
f"{filed} filed | {len(report['already_filed'])} already filed | "
f"{len(report['duplicate_number'])} duplicate number | "
f"{len(report['needs_review'])} need review"