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:
@@ -74,6 +74,12 @@ class EspoClient:
|
||||
json={"firstName": first, "lastName": last,
|
||||
"accountId": account_id})["id"]
|
||||
|
||||
def ensure_ctag(self, name):
|
||||
hits = self.search("CTag", "equals", "name", name)
|
||||
if hits:
|
||||
return hits[0]["id"]
|
||||
return self._req("POST", "CTag", json={"name": name})["id"]
|
||||
|
||||
def unique_opportunity_name(self, name):
|
||||
existing = {h["name"] for h in self.search(
|
||||
"Opportunity", "startsWith", "name", name, max_size=100)}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -47,6 +47,19 @@ def test_ensure_account_exact_match_and_create():
|
||||
assert payload == {"name": "Neue GmbH", "type": "Customer"}
|
||||
|
||||
|
||||
def test_ensure_ctag_find_and_create():
|
||||
hits = {"list": [{"id": "t1", "name": "Auto: Durch Suchagent-Treffer erstellt"}]}
|
||||
client = make_client([resp(body=hits)])
|
||||
assert client.ensure_ctag("Auto: Durch Suchagent-Treffer erstellt") == "t1"
|
||||
params = client.session.request.call_args.kwargs["params"]
|
||||
assert params["where[0][type]"] == "equals"
|
||||
assert params["where[0][attribute]"] == "name"
|
||||
client = make_client([resp(body={"list": []}), resp(body={"id": "t2"})])
|
||||
assert client.ensure_ctag("Auto: Durch Suchagent-Treffer erstellt") == "t2"
|
||||
payload = client.session.request.call_args.kwargs["json"]
|
||||
assert payload == {"name": "Auto: Durch Suchagent-Treffer erstellt"}
|
||||
|
||||
|
||||
def test_unique_opportunity_name_suffix():
|
||||
hits = {"list": [{"name": "Projekt X"}, {"name": "Projekt X (2)"}]}
|
||||
client = make_client([resp(body=hits)])
|
||||
|
||||
@@ -126,20 +126,45 @@ def test_stage_crm_agency_linking_and_verify():
|
||||
espo.team_id.return_value = "T1"
|
||||
espo.ensure_account.return_value = "A1"
|
||||
espo.ensure_contact.return_value = "C1"
|
||||
espo.ensure_ctag.return_value = "TAG1"
|
||||
espo.unique_opportunity_name.return_value = "Python Entwickler KI"
|
||||
espo.create_opportunity.return_value = {"id": "O1"}
|
||||
espo.get_opportunity.return_value = {
|
||||
"id": "O1", "name": "Python Entwickler KI",
|
||||
"cProjektlink": "https://x/projekt/p",
|
||||
"description": ctx["description"], "cAccount1Id": "A1",
|
||||
"accountId": None, "teamsIds": ["T1"]}
|
||||
"accountId": None, "teamsIds": ["T1"], "cTagsIds": ["TAG1"]}
|
||||
out = stages.stage_crm(ctx, CFG, espo=espo)
|
||||
assert out["status"] == "created" and out["opportunityId"] == "O1"
|
||||
assert out["crmUrl"].endswith("#Opportunity/view/O1")
|
||||
payload = espo.create_opportunity.call_args.args[0]
|
||||
assert payload["cAccount1Id"] == "A1" and "accountId" not in payload
|
||||
assert payload["teamsIds"] == ["T1"]
|
||||
assert payload["cTagsIds"] == ["TAG1"]
|
||||
espo.ensure_account.assert_called_with("Aristo Group", "Reseller")
|
||||
espo.ensure_ctag.assert_called_with(stages.AUTO_TAG_NAME)
|
||||
assert stages.AUTO_TAG_NAME == "Auto: Durch Suchagent-Treffer erstellt"
|
||||
|
||||
|
||||
def test_stage_crm_tag_missing_in_readback_fails_verification():
|
||||
ctx = ctx_after_match([{"nr": 1, "rating": "yes", "reason": "ok"},
|
||||
{"nr": 2, "rating": "yes", "reason": "ok"}])
|
||||
ctx = stages.stage_rules(ctx, CFG)
|
||||
espo = mock.Mock()
|
||||
espo.team_id.return_value = "T1"
|
||||
espo.ensure_account.return_value = "A1"
|
||||
espo.ensure_contact.return_value = "C1"
|
||||
espo.ensure_ctag.return_value = "TAG1"
|
||||
espo.unique_opportunity_name.return_value = "Python Entwickler KI"
|
||||
espo.create_opportunity.return_value = {"id": "O1"}
|
||||
espo.get_opportunity.return_value = {
|
||||
"id": "O1", "name": "Python Entwickler KI",
|
||||
"cProjektlink": "https://x/projekt/p",
|
||||
"description": ctx["description"], "cAccount1Id": "A1",
|
||||
"accountId": None, "teamsIds": ["T1"], "cTagsIds": []}
|
||||
out = stages.run_stage("crm", stages.stage_crm, ctx, CFG, espo=espo)
|
||||
assert out["status"] == "failed"
|
||||
assert "cTagsIds" in out["error"]
|
||||
|
||||
|
||||
def test_stage_crm_skips_when_rejected():
|
||||
|
||||
Reference in New Issue
Block a user