From 41ff08742cc3ac4da87c7ae6a6cff43da66fc359 Mon Sep 17 00:00:00 2001 From: DEls Date: Tue, 10 Nov 2026 10:12:19 +0200 Subject: [PATCH] fix(po-email): recognise a site named with an instance number MEGCPO9566's subject reads "MEGCREQ11976 (Equestria 215)". A site name with a bare instance number was not recognised as a site reference, so the PO fell through the whole name chain and came back as "no MEGA site name found" rather than naming the site it wanted. A bracketed name with an instance number is now a site reference, and a text whose bracketed names all resolve is not flagged at all. --- docs/n8n/po-email-intake.md | 9 +++++++++ scripts/po_email_intake.py | 24 +++++++++++++++--------- scripts/test_po_email_intake.py | 17 +++++++++++++++++ 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/docs/n8n/po-email-intake.md b/docs/n8n/po-email-intake.md index 544ecbb..fada147 100644 --- a/docs/n8n/po-email-intake.md +++ b/docs/n8n/po-email-intake.md @@ -234,6 +234,15 @@ bracket that is a code or a spec — `(C26/27)`, `(110mm PVC)` — is not a site trigger it. **Willow Park Ext 98** (`C25/40`) was added to the app on 2026-11-09 with its commercial details still blank, and MEGCPO9564 was moved onto it. +A site name may carry an instance number instead of an "EXT" qualifier — MEGCPO9566's subject +reads `MEGCREQ11976 (Equestria 215)`, and MEGA's own plant register calls that site *"Equestria +215"* too (buildsite lists it as *Equestria*, code *Ext 215*). **Equestria 215** (`C25/31`) was +added to the app on 2026-11-10 with its commercial details still blank. + +> **A message flagged for review has already been marked as seen.** Once the site it names has +> been added, re-run the script with `--force` so the message is checked again — a plain re-run +> skips it as already seen and the PO is never filed. + **The project code on the PO's lines is deliberately not used.** It is a cost allocation, not a statement of where the work is going. MEGCPO9550 is a **Cedar Gate** delivery — its subject says `(Cedar Gate)` and its email subject says *"RE: Cedar Gate Paving & Kerbs PO"* — yet four of its diff --git a/scripts/po_email_intake.py b/scripts/po_email_intake.py index 4560f2c..4b1cb27 100755 --- a/scripts/po_email_intake.py +++ b/scripts/po_email_intake.py @@ -330,8 +330,9 @@ def site_vocabulary(sites: list[dict]) -> set[str]: def names_a_site(candidate: str, vocabulary: set[str]) -> bool: """Is this bracketed phrase MEGA writing the site, rather than a code? - "EXT98" is MEGA's own way of marking an instance of a site, and a phrase - whose words are all ours is a site name. "(DN150)", "(T16)" and + "EXT98" is MEGA's own way of marking an instance of a site, a name with an + instance number -- "(Equestria 215)" -- is one too, and a phrase whose + words are all ours is a site name. "(DN150)", "(T16)" and "(Union fitting)" are none of those, so they are not site names. """ tokens = [t for t in re.split(r"[^A-Za-z0-9]+", candidate.lower()) if t] @@ -340,6 +341,8 @@ def names_a_site(candidate: str, vocabulary: set[str]) -> bool: if any(re.fullmatch(r"ext\d+", t) for t in tokens): return True words = [t for t in tokens if len(t) >= 4 and t.isalpha()] + if any(re.fullmatch(r"\d{2,}", t) for t in tokens): + return bool(words) return bool(words) and all(word in vocabulary for word in words) @@ -353,13 +356,16 @@ def unknown_site_name(text: str, sites: list[dict]) -> str | None: now left for a person instead. """ vocabulary = site_vocabulary(sites) - for match in BRACKETED.finditer(text or ""): - candidate = match.group(1).strip() - if not names_a_site(candidate, vocabulary): - continue - if resolve_site_by_name(candidate, sites) is None: - return candidate - return None + candidates = [ + match.group(1).strip() + for match in BRACKETED.finditer(text or "") + if names_a_site(match.group(1).strip(), vocabulary) + ] + if not candidates: + return None + if any(resolve_site_by_name(candidate, sites) for candidate in candidates): + return None + return candidates[0] def site_for_po( diff --git a/scripts/test_po_email_intake.py b/scripts/test_po_email_intake.py index a78bdfc..1cdcf43 100644 --- a/scripts/test_po_email_intake.py +++ b/scripts/test_po_email_intake.py @@ -250,6 +250,23 @@ class UnknownSiteName(unittest.TestCase): def test_a_number_in_brackets_is_not_a_site(self): self.assertIsNone(unknown_site_name("Call (012) 803 4456", SITES)) + def test_a_site_named_with_an_instance_number_is_named(self): + # MEGCPO9566: "MEGCREQ11976 (Equestria 215)". + self.assertEqual( + unknown_site_name("MEGCREQ11976 (Equestria 215)", SITES), + "Equestria 215", + ) + + def test_a_site_named_with_an_instance_number_resolves_once_it_exists(self): + sites = SITES + [{"id": "equestria", "name": "Equestria 215", "code": "C25/31"}] + self.assertIsNone(unknown_site_name("MEGCREQ11976 (Equestria 215)", sites)) + + def test_a_known_site_in_the_same_text_settles_it(self): + # One bracketed name we know means the text names a site we have. + self.assertIsNone( + unknown_site_name("Tools (Nellmapius EXT25) (Grade 40)", SITES) + ) + if __name__ == "__main__": unittest.main()