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

@@ -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)])

View File

@@ -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():