Compare commits
4 Commits
ad0131c2d9
...
2d167421b2
| Author | SHA1 | Date |
|---|---|---|
|
|
2d167421b2 | |
|
|
7aac78f3a4 | |
|
|
cabf753326 | |
|
|
785107e8de |
|
|
@ -1,5 +1,6 @@
|
|||
__pycache__/
|
||||
.env
|
||||
.env-orig
|
||||
*.pyc
|
||||
*.pyo
|
||||
.vscode/
|
||||
|
|
@ -8,3 +9,5 @@ __pycache__/
|
|||
*.b64
|
||||
downloads/
|
||||
migration-output/
|
||||
*.pdf
|
||||
private.key
|
||||
|
|
|
|||
27
README.md
27
README.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue