Compare commits
4 Commits
ad0131c2d9
...
2d167421b2
| Author | SHA1 | Date |
|---|---|---|
|
|
2d167421b2 | |
|
|
7aac78f3a4 | |
|
|
cabf753326 | |
|
|
785107e8de |
|
|
@ -1,5 +1,6 @@
|
||||||
__pycache__/
|
__pycache__/
|
||||||
.env
|
.env
|
||||||
|
.env-orig
|
||||||
*.pyc
|
*.pyc
|
||||||
*.pyo
|
*.pyo
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
@ -8,3 +9,5 @@ __pycache__/
|
||||||
*.b64
|
*.b64
|
||||||
downloads/
|
downloads/
|
||||||
migration-output/
|
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`)
|
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, 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)
|
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,24 +31,13 @@ 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
|
||||||
```
|
```
|
||||||
# Adobe Sign
|
Then fill in your credentials. See [.env-sample](.env-sample) for the full list of
|
||||||
ADOBE_CLIENT_ID=your-adobe-client-id
|
variables with descriptions. Use `account-d.docusign.com` and
|
||||||
ADOBE_CLIENT_SECRET=your-adobe-client-secret
|
`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`).
|
||||||
# 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
|
||||||
|
|
@ -107,7 +96,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,
|
||||||
including edge cases and known gaps.
|
conditional logic mapping, and known gaps.
|
||||||
|
|
||||||
## Known API quirks and bugs
|
## 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
|
`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 logic**: Adobe Sign conditional show/hide rules are not mapped.
|
- **Conditional HIDE**: Adobe Sign can conditionally hide a field. DocuSign only supports
|
||||||
DocuSign supports conditional tabs but the logic structure differs — manual
|
revealing hidden fields — there is no native way to hide a visible field conditionally.
|
||||||
rewrite required per template.
|
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.
|
- **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.
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,14 @@ 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
|
||||||
|
|
@ -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]
|
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
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -309,6 +383,7 @@ 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 = {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,18 @@ 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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue