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:
parent
607de20232
commit
de8aa83ef1
2 changed files with 36 additions and 6 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue