diff --git a/docs/n8n/po-email-intake.md b/docs/n8n/po-email-intake.md index 1c286a4..59d7ac6 100644 --- a/docs/n8n/po-email-intake.md +++ b/docs/n8n/po-email-intake.md @@ -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. diff --git a/scripts/po_email_intake.py b/scripts/po_email_intake.py index 5ffe3f3..db3ed73 100755 --- a/scripts/po_email_intake.py +++ b/scripts/po_email_intake.py @@ -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"