Compare commits

...

4 Commits

Author SHA1 Message Date
Paul Huliganga 2d167421b2 chore: gitignore PDFs, private.key, and .env-orig
PDFs are generated artifacts. private.key and .env-orig are secrets
that should never be tracked.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 12:35:20 -04:00
Paul Huliganga 7aac78f3a4 docs: document Adobe Sign non-standard refresh token endpoint
Adobe Sign uses /oauth/v2/refresh (not /oauth/v2/token) for token
refresh — a deviation from the OAuth2 spec that caused all refresh
attempts to fail with a misleading "Invalid grant_type" error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 12:35:17 -04:00
Paul Huliganga cabf753326 docs: update README — reference .env-sample and mention conditional logic
- Setup step 2 now directs users to copy .env-sample instead of
  repeating all variables inline
- "What it does" step 3 mentions conditional field logic
- Field type mapping section updated to mention conditional logic

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 12:35:13 -04:00
Paul Huliganga 785107e8de feat: map Adobe Sign conditional logic to DocuSign conditionalParentLabel/Value
Adobe Sign conditionalAction (SHOW/EQUALS) is now translated to
DocuSign's conditionalParentLabel + conditionalParentValue on the
dependent tab, making conditional fields work in the migrated template.

For radio groups, conditionalParentLabel matches the radio group name.

Unsupported cases emit warnings rather than silently dropping conditions:
- HIDE action (no DocuSign equivalent — field left always visible)
- Non-EQUALS operators (skipped)
- Multi-predicate ANY/ALL (first EQUALS predicate used, rest ignored)

Also updates field-mapping.md: adds Conditional Logic Mapping table
and moves this item out of Known Gaps into documented behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 12:18:48 -04:00
5 changed files with 118 additions and 22 deletions

3
.gitignore vendored
View File

@ -1,5 +1,6 @@
__pycache__/
.env
.env-orig
*.pyc
*.pyo
.vscode/
@ -8,3 +9,5 @@ __pycache__/
*.b64
downloads/
migration-output/
*.pdf
private.key

View File

@ -9,7 +9,7 @@ It downloads templates via the Adobe Sign API, converts them to DocuSign format,
1. **Authenticates** with Adobe Sign via OAuth (one-time browser flow, tokens saved to `.env`)
2. **Downloads** templates — PDF, metadata, and form field definitions
3. **Converts** each template to a DocuSign `envelopeTemplate` JSON, mapping all field types, coordinates, and recipient roles
3. **Converts** each template to a DocuSign `envelopeTemplate` JSON, mapping all field types, coordinates, recipient roles, and conditional field logic
4. **Authenticates** with DocuSign via JWT grant (one-time browser consent, then fully automated)
5. **Uploads** the converted template to DocuSign via the REST API
@ -31,24 +31,13 @@ pip install -r requirements.txt
```
**2. Create a `.env` file** in the project root (never commit this):
```bash
cp .env-sample .env
```
# Adobe Sign
ADOBE_CLIENT_ID=your-adobe-client-id
ADOBE_CLIENT_SECRET=your-adobe-client-secret
# DocuSign
DOCUSIGN_CLIENT_ID=your-integration-key
DOCUSIGN_CLIENT_SECRET=your-client-secret
DOCUSIGN_USER_ID=your-docusign-user-guid
DOCUSIGN_ACCOUNT_ID=your-docusign-account-id
DOCUSIGN_PRIVATE_KEY_PATH=/path/to/private.key
DOCUSIGN_AUTH_SERVER=account-d.docusign.com
DOCUSIGN_BASE_URL=https://demo.docusign.net/restapi
DOCUSIGN_REDIRECT_URI=http://localhost:8080/callback
```
Use `account-d.docusign.com` and `https://demo.docusign.net/restapi` for sandbox.
For production replace with `account.docusign.com` and your account's base URL (e.g. `https://na3.docusign.net/restapi`).
Then fill in your credentials. See [.env-sample](.env-sample) for the full list of
variables with descriptions. Use `account-d.docusign.com` and
`https://demo.docusign.net/restapi` for sandbox; for production replace with
`account.docusign.com` and your account's base URL (e.g. `https://na3.docusign.net/restapi`).
**3. Authenticate with Adobe Sign** (one-time):
```bash
@ -107,7 +96,7 @@ If multiple templates share the same name, the most recently modified one is use
## Field type mapping
See [field-mapping.md](field-mapping.md) for the full Adobe Sign → DocuSign tab type table,
including edge cases and known gaps.
conditional logic mapping, and known gaps.
## Known API quirks and bugs

View File

@ -80,11 +80,28 @@ Tab types that do not merge (only first location used or handled specially):
`radioGroupTabs` — each location is one radio button within the group
`signerAttachmentTabs` — each location is an independent attachment request
## Conditional Logic Mapping
Adobe Sign `conditionalAction` → DocuSign `conditionalParentLabel` + `conditionalParentValue` on the dependent tab.
| Adobe Sign | DocuSign | Notes |
|-----------------------------------|---------------------------------|-------|
| `predicates[].fieldName` | `conditionalParentLabel` | For radio groups, matches the group name |
| `predicates[].value` | `conditionalParentValue` | The value the trigger must equal to reveal the tab |
| `action: SHOW` | Supported | Tab is hidden until condition is met |
| `action: HIDE` | **Not supported** | No DocuSign equivalent — condition skipped, field always shown |
| `operator: EQUALS` | Supported | Only operator DocuSign supports |
| Other operators | **Not supported** | Condition skipped, warning logged |
| Multiple predicates (ANY/ALL) | **Partial** — first EQUALS only | Warning logged; remaining predicates ignored |
## Known Gaps
- **Conditional logic**: Adobe Sign conditional show/hide rules are not mapped.
DocuSign supports conditional tabs but the logic structure differs — manual
rewrite required per template.
- **Conditional HIDE**: Adobe Sign can conditionally hide a field. DocuSign only supports
revealing hidden fields — there is no native way to hide a visible field conditionally.
Templates with HIDE conditions will have those fields always visible after migration.
- **Multi-predicate conditions**: Adobe Sign supports ANY/ALL of multiple predicates.
DocuSign only supports a single parent condition per tab. Only the first EQUALS
predicate is mapped; complex conditions require manual rework.
- **DocuSign formula fields**: No Adobe Sign equivalent — flag for manual rewrite.
- **Advanced field validation**: Adobe regex/custom script validation is not mapped;
best-effort via standard DocuSign validation types only.

View File

@ -23,6 +23,14 @@ Field type coverage:
Partial: FILE_CHOOSER signerAttachmentTabs (with warning)
STAMP stampTabs (requires stamp feature enabled on DocuSign account warning logged)
Skipped: INLINE_IMAGE, PARTICIPATION_STAMP (no DocuSign equivalent warning logged)
Conditional logic:
Mapped: Single-predicate SHOW conditions (EQUALS operator)
conditionalParentLabel + conditionalParentValue on the dependent tab.
For radio groups the parentLabel matches the radio group name.
Warnings: Multi-predicate ANY/ALL conditions (only first EQUALS predicate mapped),
HIDE action (not supported in DocuSign condition skipped),
non-EQUALS operators (skipped).
"""
import base64
@ -142,6 +150,72 @@ def _sized_tabs(locations: list, label: str, extra: dict | None = None) -> list:
return [_make_sized_tab(loc, label, extra) for loc in locations]
# ---------------------------------------------------------------------------
# Conditional logic
# ---------------------------------------------------------------------------
def _apply_conditional_to_tabs(tabs: dict, field: dict, warnings: list) -> dict:
"""
Apply DocuSign conditionalParentLabel / conditionalParentValue to tabs based
on an Adobe Sign conditionalAction.
Adobe Sign model:
conditionalAction.predicates[].{fieldName, operator, value}, action=SHOW|HIDE
DocuSign model (on the dependent tab):
conditionalParentLabel tabLabel or radioGroup groupName of the trigger tab
conditionalParentValue value the trigger must have to reveal this tab
Mapping limitations:
- Only SHOW action is supported. DocuSign has no native HIDE condition skipped.
- Only EQUALS operator is supported. Others are skipped.
- Only one predicate is mapped. Multi-predicate ANY/ALL logic is not supported;
the first EQUALS predicate is used and a warning is logged.
"""
if not tabs:
return tabs
ca = field.get("conditionalAction", {})
predicates = ca.get("predicates", [])
if not predicates:
return tabs # No conditions — field is always visible
label = field.get("name", "unnamed")
action = ca.get("action", "SHOW")
if action != "SHOW":
warnings.append(
f"Conditional '{label}': action={action} is not supported in DocuSign "
f"(only SHOW is supported) — condition skipped"
)
return tabs
predicate = next((p for p in predicates if p.get("operator") == "EQUALS"), None)
if not predicate:
warnings.append(
f"Conditional '{label}': no EQUALS predicate found "
f"(operators: {[p.get('operator') for p in predicates]}) — condition skipped"
)
return tabs
if len(predicates) > 1:
warnings.append(
f"Conditional '{label}': {len(predicates)} predicates with "
f"anyOrAll={ca.get('anyOrAll')} — only first EQUALS predicate mapped, "
f"remaining conditions ignored"
)
parent_label = predicate["fieldName"]
parent_value = predicate["value"]
for tab_list in tabs.values():
for tab in tab_list:
tab["conditionalParentLabel"] = parent_label
tab["conditionalParentValue"] = parent_value
return tabs
# ---------------------------------------------------------------------------
# Tab builder
# ---------------------------------------------------------------------------
@ -309,6 +383,7 @@ def compose_template(template_dir: str, output_path: str) -> tuple[dict, list[st
if idx >= len(signers):
idx = 0
tabs = build_tabs_for_field(field, warnings)
tabs = _apply_conditional_to_tabs(tabs, field, warnings)
signers[idx]["tabs"] = merge_tabs(signers[idx]["tabs"], tabs)
template = {

View File

@ -19,6 +19,18 @@ editor after upload.
## Adobe Sign API Quirks
### Token refresh uses a separate endpoint `/oauth/v2/refresh` (2026-04-16)
**Symptom:** Calling the standard OAuth2 token endpoint (`/oauth/v2/token`) with
`grant_type=refresh_token` returns `{"error":"invalid_request","error_description":
"Invalid grant_type refresh_token"}` — even with a valid, freshly-issued refresh token.
**Root cause:** Adobe Sign uses a non-standard separate endpoint for token refresh:
`/oauth/v2/refresh` (not `/oauth/v2/token`). This differs from the OAuth2 spec and
virtually all other OAuth2 providers.
**Fix applied:** `adobe_api.py` now uses `REFRESH_URL = .../oauth/v2/refresh` for
refresh requests and `TOKEN_URL = .../oauth/v2/token` only for the initial auth code
exchange.
**Note:** `redirect_uri` is not required in the refresh request and should be omitted.
### Company/Title contentType returned with `SIGNER_` prefix (2026-04-15)
**Symptom:** When Company and Title fields are set via the Adobe Sign UI (the API
rejects `COMPANY`/`TITLE` as contentType values), the API returns them as