feat(projekt-matching): auto-tag created opportunities, add German docs (S68)

Every opportunity created by the workflow now carries the CTag
"Auto: Durch Suchagent-Treffer erstellt" (id 6a83292a186dce7b2), set via
cTagsIds directly in the create POST. The tag id is resolved at runtime
by name (EspoClient.ensure_ctag, same pattern as team_id; self-healing
after a CRM rebuild); the read-back verification now also checks the tag.
No backfill: existing opportunities and the saved "Via cowork-api
erstellt" list filter are untouched.

Live-verified end to end: synthetic trigger mail, opportunity
6a832b8f29cc86bdf created with cTagsNames containing the tag (test
artifact removed afterwards). 65/65 pytest.

Also adds the complete German technical documentation of the workflow
(docs/projekt-matching-dokumentation.md, sections 1-13 + S68 appendix).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
tlg
2026-08-17 18:09:06 +02:00
parent 58b64c6d5f
commit 86bd7f6a07
5 changed files with 436 additions and 2 deletions

View File

@@ -19,6 +19,10 @@ from .mailparse import BROWSER_UA
TEAM_BY_OFFER = {"Projekt": "DesTEngS",
"Arbeitnehmer-Angebot": "Arbeitnehmer",
"ANÜ": "ANÜ"}
# Fester CTag für maschinell angelegte Verkaufschancen (Vorgabe Thomas,
# Wortlaut exakt). Auflösung zur Laufzeit über den Namen — wie team_id():
# IDs überleben keine CRM-Neuaufsetzung, der Name ist der Vertrag.
AUTO_TAG_NAME = "Auto: Durch Suchagent-Treffer erstellt"
MIN_PAGE_CHARS = 200
QUOTES = str.maketrans({c: "'" for c in "„“”\"«»‹›"})
@@ -132,8 +136,10 @@ def stage_crm(ctx, cfg, espo=None):
first, last = espocrm.split_person(ex["contactPerson"])
contact_id = espo.ensure_contact(first, last, account_id)
name = espo.unique_opportunity_name(ex["projectName"])
tag_id = espo.ensure_ctag(AUTO_TAG_NAME)
payload = {"name": name, "description": ctx["description"],
"cProjektlink": ctx["canonical"], "teamsIds": [team]}
"cProjektlink": ctx["canonical"], "teamsIds": [team],
"cTagsIds": [tag_id]}
if account_id:
payload["cAccount1Id" if agency else "accountId"] = account_id
if contact_id:
@@ -150,6 +156,8 @@ def stage_crm(ctx, cfg, espo=None):
problems.append("description")
if team not in (back.get("teamsIds") or []):
problems.append("teamsIds")
if tag_id not in (back.get("cTagsIds") or []):
problems.append("cTagsIds")
if account_id and agency and back.get("cAccount1Id") != account_id:
problems.append("cAccount1Id")
if account_id and not agency and back.get("accountId") != account_id: