This commit implements the full inbound quote email processing workflow and atomic quote numbering: - Add PocketBase collections for monthly atomic quote counters, email attachments, and threaded replies - Add idempotent migration script to set up new collections and add deduplication field to existing quotes table - Implement `/api/v1/quotes/next-number` endpoint for properly formatted Q-YYYY-MM-NNN quote numbers - Add MCP tools for idempotent quote creation, reply threading, and message ID lookup - Add end-to-end and concurrent smoke tests for pipeline validation - Update TypeScript types for new quote-related entities - Update project documentation and package.json scripts - Mark quote email intake as complete in the parity backlog - Add n8n v2 workflows for automated email monitoring and error alerts
51 lines
2.3 KiB
Python
51 lines
2.3 KiB
Python
"""Verify v5 workflow correctness."""
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
|
|
WF = Path("/Volumes/EXT/DEVELOPMENT/WFDASH_NEXTJS/n8n_workfows/WFDASH — Quote Email Monitor (v5 catch-and-create).json")
|
|
wf = json.loads(WF.read_text())
|
|
|
|
print("=== Verifying context-flow fixes ===")
|
|
for n in wf["nodes"]:
|
|
if n["name"] in ("Plan: Upload Attachments Early", "Collect Attachment IDs", "Parse PB Auth"):
|
|
code = n["parameters"]["jsCode"]
|
|
reads = re.findall(r"\$\(\s*['\"]([^'\"]+)['\"]", code)
|
|
print(f"\n{n['name']} reads from upstream nodes: {reads}")
|
|
if "binary:" in code or "binary " in code:
|
|
print(" preserves binary: yes")
|
|
else:
|
|
print(" preserves binary: NO")
|
|
|
|
print("\n=== Verifying Link Attachments to Quote ===")
|
|
for n in wf["nodes"]:
|
|
if n["name"] == "Link Attachments to Quote":
|
|
code = n["parameters"]["jsCode"]
|
|
print("Reads from upstream nodes:", re.findall(r"\$\(\s*['\"]([^'\"]+)['\"]", code))
|
|
print("Uses _attachment_record_ids:", "_attachment_record_ids" in code)
|
|
|
|
print("\n=== Connection sanity (Catch and Create branch) ===")
|
|
chain = ["PocketBase Admin Auth", "Plan: Upload Attachments Early", "Upload Attachment Early",
|
|
"Collect Attachment IDs", "Parse PB Auth"]
|
|
for i, src in enumerate(chain[:-1]):
|
|
nxt = chain[i + 1]
|
|
edges = wf["connections"].get(src, {}).get("main", [[]])
|
|
targets = [e.get("node") for e in (edges[0] if edges else [])]
|
|
ok = "✓" if nxt in targets else "✗"
|
|
print(f" {ok} {src} -> {targets} (expected {nxt})")
|
|
|
|
print("\n=== Connection sanity (Late link branch) ===")
|
|
chain2 = ["Create Quote Item", "Link Attachments to Quote", "PATCH Attachment (set quote)", "Mark has_attachments"]
|
|
for i, src in enumerate(chain2[:-1]):
|
|
nxt = chain2[i + 1]
|
|
edges = wf["connections"].get(src, {}).get("main", [[]])
|
|
targets = [e.get("node") for e in (edges[0] if edges else [])]
|
|
ok = "✓" if nxt in targets else "✗"
|
|
print(f" {ok} {src} -> {targets} (expected {nxt})")
|
|
|
|
print("\n=== Old broken nodes disabled & detached ===")
|
|
for n in wf["nodes"]:
|
|
if n["name"].startswith("DISABLED"):
|
|
in_conns = n["name"] in wf["connections"]
|
|
print(f" {'✓' if n.get('disabled') else '✗'} {n['name']} (disabled flag set)")
|
|
print(f" {'✓' if not in_conns else '✗'} not wired in connections map")
|