Compare commits

..

No commits in common. "2d167421b29cb9c692ac9d36d72ee3f7a6ecf1a5" and "ad0131c2d956ab699c3cd51b8a96175f0cb05ecc" have entirely different histories.

5 changed files with 22 additions and 118 deletions

3
.gitignore vendored
View File

@ -1,6 +1,5 @@
__pycache__/ __pycache__/
.env .env
.env-orig
*.pyc *.pyc
*.pyo *.pyo
.vscode/ .vscode/
@ -9,5 +8,3 @@ __pycache__/
*.b64 *.b64
downloads/ downloads/
migration-output/ 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`) 1. **Authenticates** with Adobe Sign via OAuth (one-time browser flow, tokens saved to `.env`)
2. **Downloads** templates — PDF, metadata, and form field definitions 2. **Downloads** templates — PDF, metadata, and form field definitions
3. **Converts** each template to a DocuSign `envelopeTemplate` JSON, mapping all field types, coordinates, recipient roles, and conditional field logic 3. **Converts** each template to a DocuSign `envelopeTemplate` JSON, mapping all field types, coordinates, and recipient roles
4. **Authenticates** with DocuSign via JWT grant (one-time browser consent, then fully automated) 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 5. **Uploads** the converted template to DocuSign via the REST API
@ -31,13 +31,24 @@ pip install -r requirements.txt
``` ```
**2. Create a `.env` file** in the project root (never commit this): **2. Create a `.env` file** in the project root (never commit this):
```bash
cp .env-sample .env
``` ```
Then fill in your credentials. See [.env-sample](.env-sample) for the full list of # Adobe Sign
variables with descriptions. Use `account-d.docusign.com` and ADOBE_CLIENT_ID=your-adobe-client-id
`https://demo.docusign.net/restapi` for sandbox; for production replace with ADOBE_CLIENT_SECRET=your-adobe-client-secret
`account.docusign.com` and your account's base URL (e.g. `https://na3.docusign.net/restapi`).
# 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`).
**3. Authenticate with Adobe Sign** (one-time): **3. Authenticate with Adobe Sign** (one-time):
```bash ```bash
@ -96,7 +107,7 @@ If multiple templates share the same name, the most recently modified one is use
## Field type mapping ## Field type mapping
See [field-mapping.md](field-mapping.md) for the full Adobe Sign → DocuSign tab type table, See [field-mapping.md](field-mapping.md) for the full Adobe Sign → DocuSign tab type table,
conditional logic mapping, and known gaps. including edge cases and known gaps.
## Known API quirks and bugs ## Known API quirks and bugs

View File

@ -80,28 +80,11 @@ Tab types that do not merge (only first location used or handled specially):
`radioGroupTabs` — each location is one radio button within the group `radioGroupTabs` — each location is one radio button within the group
`signerAttachmentTabs` — each location is an independent attachment request `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 ## Known Gaps
- **Conditional HIDE**: Adobe Sign can conditionally hide a field. DocuSign only supports - **Conditional logic**: Adobe Sign conditional show/hide rules are not mapped.
revealing hidden fields — there is no native way to hide a visible field conditionally. DocuSign supports conditional tabs but the logic structure differs — manual
Templates with HIDE conditions will have those fields always visible after migration. rewrite required per template.
- **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. - **DocuSign formula fields**: No Adobe Sign equivalent — flag for manual rewrite.
- **Advanced field validation**: Adobe regex/custom script validation is not mapped; - **Advanced field validation**: Adobe regex/custom script validation is not mapped;
best-effort via standard DocuSign validation types only. best-effort via standard DocuSign validation types only.

View File

@ -23,14 +23,6 @@ Field type coverage:
Partial: FILE_CHOOSER signerAttachmentTabs (with warning) Partial: FILE_CHOOSER signerAttachmentTabs (with warning)
STAMP stampTabs (requires stamp feature enabled on DocuSign account warning logged) STAMP stampTabs (requires stamp feature enabled on DocuSign account warning logged)
Skipped: INLINE_IMAGE, PARTICIPATION_STAMP (no DocuSign equivalent 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 import base64
@ -150,72 +142,6 @@ def _sized_tabs(locations: list, label: str, extra: dict | None = None) -> list:
return [_make_sized_tab(loc, label, extra) for loc in locations] 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 # Tab builder
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -383,7 +309,6 @@ def compose_template(template_dir: str, output_path: str) -> tuple[dict, list[st
if idx >= len(signers): if idx >= len(signers):
idx = 0 idx = 0
tabs = build_tabs_for_field(field, warnings) tabs = build_tabs_for_field(field, warnings)
tabs = _apply_conditional_to_tabs(tabs, field, warnings)
signers[idx]["tabs"] = merge_tabs(signers[idx]["tabs"], tabs) signers[idx]["tabs"] = merge_tabs(signers[idx]["tabs"], tabs)
template = { template = {

View File

@ -19,18 +19,6 @@ editor after upload.
## Adobe Sign API Quirks ## 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) ### 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 **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 rejects `COMPANY`/`TITLE` as contentType values), the API returns them as