diff --git a/src/compose_docusign_template.py b/src/compose_docusign_template.py index 7997806..db1393f 100644 --- a/src/compose_docusign_template.py +++ b/src/compose_docusign_template.py @@ -325,6 +325,46 @@ def merge_tabs(acc: dict, new: dict) -> dict: return acc +# Tab types DocuSign forbids as conditional parents (auto-filled or action tabs) +_INVALID_PARENT_TAB_TYPES = { + "signHereTabs", "initialHereTabs", "dateSignedTabs", + "fullNameTabs", "emailTabs", "titleTabs", "signerAttachmentTabs", +} + + +def _strip_invalid_conditionals(signers: list, warnings: list) -> None: + """ + Remove conditionalParentLabel/Value from any tab whose parent label either + doesn't exist in the template or points to a tab type DocuSign forbids as a + parent (signature, initial, auto-filled). Mutates signers in place. + """ + for signer in signers: + tabs = signer.get("tabs", {}) + + # Collect valid parent labels: only tab types allowed as parents + valid_labels: set[str] = set() + for tab_type, tab_list in tabs.items(): + if tab_type in _INVALID_PARENT_TAB_TYPES: + continue + for tab in tab_list: + label = tab.get("tabLabel") or tab.get("groupName") + if label: + valid_labels.add(label) + + # Strip references to invalid/missing parents + for tab_type, tab_list in tabs.items(): + for tab in tab_list: + parent = tab.get("conditionalParentLabel") + if parent and parent not in valid_labels: + warnings.append( + f"Conditional parent '{parent}' not found or not a valid " + f"parent tab type — conditional stripped from tab " + f"'{tab.get('tabLabel', '?')}'" + ) + tab.pop("conditionalParentLabel", None) + tab.pop("conditionalParentValue", None) + + # --------------------------------------------------------------------------- # Main compose function # --------------------------------------------------------------------------- @@ -386,6 +426,10 @@ def compose_template(template_dir: str, output_path: str) -> tuple[dict, list[st tabs = _apply_conditional_to_tabs(tabs, field, warnings) signers[idx]["tabs"] = merge_tabs(signers[idx]["tabs"], tabs) + # Post-process: strip conditionalParentLabel references that point to + # non-existent or invalid parents (signature/initial tabs can't be parents). + _strip_invalid_conditionals(signers, warnings) + template = { "name": metadata.get("name", template_dir.name), "description": f"Migrated from Adobe Sign — original owner: {metadata.get('ownerEmail', '')}", diff --git a/tests/test_api_verify.py b/tests/test_api_verify.py index af74361..37189bb 100644 --- a/tests/test_api_verify.py +++ b/tests/test_api_verify.py @@ -51,7 +51,14 @@ class TestVerifySend: @respx.mock def test_send_returns_envelope_id(self): - """Authenticated + valid template → envelope_id returned.""" + """Authenticated + valid template → role names fetched, envelope_id returned.""" + respx.get( + f"{DS_BASE}/v2.1/accounts/{DS_ACCOUNT}/templates/{TEMPLATE_ID}" + ).mock(return_value=httpx.Response(200, json={ + "recipients": { + "signers": [{"roleName": "Customer", "recipientId": "1"}], + } + })) respx.post( f"{DS_BASE}/v2.1/accounts/{DS_ACCOUNT}/envelopes" ).mock(return_value=httpx.Response(201, json={"envelopeId": ENVELOPE_ID})) @@ -67,10 +74,32 @@ class TestVerifySend: ) assert resp.status_code == 200 assert resp.json()["envelope_id"] == ENVELOPE_ID + assert resp.json()["roles"] == ["Customer"] + + @respx.mock + def test_send_falls_back_to_signer_role_on_template_error(self): + """Template fetch failure → falls back to 'Signer' role name.""" + respx.get( + f"{DS_BASE}/v2.1/accounts/{DS_ACCOUNT}/templates/bad-id" + ).mock(return_value=httpx.Response(404, json={"message": "Not found"})) + respx.post( + f"{DS_BASE}/v2.1/accounts/{DS_ACCOUNT}/envelopes" + ).mock(return_value=httpx.Response(201, json={"envelopeId": ENVELOPE_ID})) + + resp = client.post( + "/api/verify/send", + json={"template_id": "bad-id", "recipient_name": "X", "recipient_email": "x@x.com"}, + cookies={_COOKIE_NAME: _ds_session()}, + ) + assert resp.status_code == 200 + assert resp.json()["roles"] == ["Signer"] @respx.mock def test_send_propagates_docusign_error(self): - """DocuSign 400 → 502 with error detail.""" + """DocuSign 400 on envelope create → 502 with error detail.""" + respx.get( + f"{DS_BASE}/v2.1/accounts/{DS_ACCOUNT}/templates/bad-id" + ).mock(return_value=httpx.Response(200, json={"recipients": {}})) respx.post( f"{DS_BASE}/v2.1/accounts/{DS_ACCOUNT}/envelopes" ).mock(return_value=httpx.Response(400, json={"message": "Invalid templateId"}))