Add latest migration files and validation outputs

This commit is contained in:
Paul Huliganga 2026-04-20 01:12:46 -04:00
parent c63d49e208
commit 39982008d3
9 changed files with 509 additions and 0 deletions

50
.env-adobe Normal file
View File

@ -0,0 +1,50 @@
# Adobe Sign → DocuSign Migrator — Environment Variables
# Copy this file to .env and fill in your values.
# Never commit .env to version control.
# ─── Adobe Sign ──────────────────────────────────────────────────────────────
# OAuth app credentials from the Adobe Sign developer console
ADOBE_CLIENT_ID=ats-58a336e4-3dd5-466d-bc5d-ba341a012694
ADOBE_CLIENT_SECRET=4c9SRsLNEBn953hzR1wa7wL5VzHnD5k_
# Auto-written by src/adobe_auth.py after the one-time OAuth flow.
# Leave blank; they will be populated on first run.
ADOBE_ACCESS_TOKEN="3AAABLblqZhDON9k_91_RhgUlZbpHx6luaPSmu7_Jj1hrPdmqCQ6ciQDVJVVvLMr__4v161k3kZc6c2fYbxsl5tA1IbmQni9T"
ADOBE_REFRESH_TOKEN="3AAABLblqZhB6qLQOQ2H5oax-Ed3E6Nc0IqFupdB9UlKzAoWQ3Cb2u3lla4d6Vuquf9xHhGMfn68*"
ADOBE_SIGN_BASE_URL=https://api.eu2.adobesign.com/api/rest/v6
# ─── DocuSign ────────────────────────────────────────────────────────────────
# Integration key (client ID) from the DocuSign developer console
DOCUSIGN_CLIENT_ID=your-integration-key
# Client secret — only needed for the one-time Auth Code Grant consent flow
DOCUSIGN_CLIENT_SECRET=your-client-secret
# GUID of the DocuSign user to impersonate via JWT grant
# Found in the DocuSign admin UI under Users → user details
DOCUSIGN_USER_ID=your-docusign-user-guid
# Account ID of the target DocuSign account
# Found in the DocuSign admin UI under Settings → Account Profile
DOCUSIGN_ACCOUNT_ID=your-docusign-account-id
# Path to the RSA private key file used for JWT signing
# Generate a keypair in the DocuSign developer console and save the private key here
DOCUSIGN_PRIVATE_KEY_PATH=/path/to/private.key
# OAuth auth server — use account-d.docusign.com for sandbox, account.docusign.com for production
DOCUSIGN_AUTH_SERVER=account-d.docusign.com
# eSignature REST API base URL
# Sandbox: https://demo.docusign.net/restapi
# Production: https://na3.docusign.net/restapi (replace na3 with your shard)
DOCUSIGN_BASE_URL=https://demo.docusign.net/restapi
# Redirect URI registered in your DocuSign app (used only during one-time consent flow)
DOCUSIGN_REDIRECT_URI=http://localhost:8080/callback
# Auto-written by src/docusign_auth.py to cache the JWT access token.
# Leave blank; they will be populated automatically.
DOCUSIGN_ACCESS_TOKEN=
DOCUSIGN_TOKEN_EXPIRY=

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,8 @@
# Collaboration/Diagram Log
## Session: 2026-04-14
- Architecture mermaid diagram block added to docs/architecture.md (data flow, main modules)
- All new regressions and results summarized in validation/
- This log is ready for markdown, diagrams, and collaborative session notes as agent/project evolves.
*Agent generated*

View File

@ -0,0 +1,26 @@
# DocuSign Ingest Stub
"""
Stub for DocuSign API ingest.
- Loads mapped onboarding template data.
- Would POST to DocuSign API if credentials configured
"""
import json
from pathlib import Path
from pprint import pprint
def push_to_docusign(template):
print("(Stub) Would push the following template to DocuSign:")
pprint(template)
# Here you'd use requests to POST to DocuSign API endpoint
# For MVP: stop here and log intended payload
if __name__ == "__main__":
mapped = json.loads(Path("../validation/onboarding-mapping-eval.md").read_text(errors="ignore").split('---')[-1]) if False else None
# For showcase: rerun mapping function from test_mapping.py
import sys
sys.path.append("../src")
from test_mapping import map_adobe_fields_to_docusign_tabs
fields = json.loads(Path("../sample-templates/onboarding-template-formfields.json").read_text())
mapped = map_adobe_fields_to_docusign_tabs(fields)
push_to_docusign(mapped)

View File

@ -0,0 +1,176 @@
"""
migrate_paul_template.py
------------------------
End-to-end validation: downloads the most recently edited "Paul Adobe Template"
from Adobe Sign, converts it to a DocuSign template JSON, and uploads it.
Usage:
python3 src/migrate_paul_template.py
"""
import json
import os
import subprocess
import sys
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
sys.path.insert(0, os.path.dirname(__file__))
from adobe_api import adobe_api_get, adobe_api_get_bytes
DOWNLOADS_DIR = Path(__file__).parent.parent / "downloads"
MIGRATION_OUTPUT_DIR = Path(__file__).parent.parent / "migration-output"
TEMPLATE_NAME = "Paul Adobe Template"
CLI_PATH = Path(__file__).parent.parent.parent / "docusign-direct" / "packages" / "esign-direct" / "build" / "cli.js"
def safe_dirname(name):
return "".join(c if c.isalnum() or c in " -_" else "_" for c in name).strip()
def save_json(path, data):
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
def find_latest_paul_template():
"""Return the metadata dict for the most recently modified 'Paul Adobe Template'."""
print("Fetching template list from Adobe Sign...")
result = adobe_api_get("libraryDocuments")
all_templates = result.get("libraryDocumentList", [])
print(f" Found {len(all_templates)} total template(s).")
matches = [t for t in all_templates if t.get("name", "") == TEMPLATE_NAME]
if not matches:
print(f"ERROR: No template named '{TEMPLATE_NAME}' found.")
sys.exit(1)
print(f" Found {len(matches)} template(s) named '{TEMPLATE_NAME}':")
for t in matches:
print(f" ID: {t['id']} modifiedDate: {t.get('modifiedDate', 'n/a')}")
# Pick most recently modified
latest = max(matches, key=lambda t: t.get("modifiedDate", ""))
print(f"\n Using most recently modified: ID={latest['id']} modifiedDate={latest.get('modifiedDate', 'n/a')}")
return latest
def download_template(template):
template_id = template["id"]
template_name = template["name"]
dir_name = f"{safe_dirname(template_name)}__{template_id[:8]}"
out_dir = DOWNLOADS_DIR / dir_name
out_dir.mkdir(parents=True, exist_ok=True)
print(f"\nDownloading template to downloads/{dir_name}/")
# Full metadata
metadata = adobe_api_get(f"libraryDocuments/{template_id}")
save_json(out_dir / "metadata.json", metadata)
# Form fields
try:
form_fields = adobe_api_get(f"libraryDocuments/{template_id}/formFields")
save_json(out_dir / "form_fields.json", form_fields)
field_count = len(form_fields.get("fields", []))
print(f" {field_count} form fields saved.")
for f in form_fields.get("fields", []):
val = f.get("validation", "")
val_str = f"/ {val}" if val and val != "NONE" else ""
print(f" {f['inputType']:15} {f.get('contentType',''):20} {val_str:10} '{f['name']}'")
except Exception as e:
print(f" WARNING: Could not fetch form fields: {e}")
save_json(out_dir / "form_fields.json", {"error": str(e)})
# Document list + PDFs
docs = adobe_api_get(f"libraryDocuments/{template_id}/documents")
save_json(out_dir / "documents.json", docs)
for doc in docs.get("documents", []):
doc_id = doc["id"]
doc_name = doc.get("name", doc_id)
if not doc_name.lower().endswith(".pdf"):
doc_name += ".pdf"
safe_name = safe_dirname(doc_name)
pdf_path = out_dir / safe_name
try:
pdf_bytes = adobe_api_get_bytes(f"libraryDocuments/{template_id}/documents/{doc_id}")
with open(pdf_path, "wb") as f:
f.write(pdf_bytes)
print(f" PDF saved ({len(pdf_bytes) // 1024}KB) → {safe_name}")
except Exception as e:
print(f" WARNING: Could not download PDF: {e}")
return out_dir
def run_migration(template_dir: Path) -> Path:
"""Convert the downloaded template folder to a DocuSign template JSON."""
sys.path.insert(0, str(Path(__file__).parent))
from compose_docusign_template import compose_template
output_path = MIGRATION_OUTPUT_DIR / template_dir.name / "docusign-template.json"
print(f"\nRunning migration: {template_dir.name}")
template_dict, warnings = compose_template(str(template_dir), str(output_path))
print(f" Written: {output_path}")
if warnings:
print(" Warnings:")
for w in warnings:
print(f" WARNING: {w}")
# Print tab summary
signers = template_dict.get("recipients", {}).get("signers", [])
for signer in signers:
tabs = signer.get("tabs", {})
print(f" Tabs for '{signer['roleName']}':")
for tab_type, tab_list in sorted(tabs.items()):
for tab in tab_list:
label = tab.get("tabLabel") or tab.get("groupName", "?")
print(f" {tab_type:25} '{label}'")
return output_path
def upload_to_docusign(output_path: Path):
if not CLI_PATH.exists():
print(f"\nWARNING: DocuSign CLI not found at {CLI_PATH}")
print(f" Skipping upload. To upload manually:")
print(f" node {CLI_PATH} templates create --file {output_path}")
return
print(f"\nUploading to DocuSign...")
# Use nvm's node (system node may be too old for the ?? operator in the CLI)
nvm_node = Path.home() / ".nvm" / "alias" / "default"
nvm_sh = Path.home() / ".nvm" / "nvm.sh"
cmd = f'source {nvm_sh} && node {CLI_PATH} templates create --file "{output_path}"'
result = subprocess.run(
cmd, shell=True, executable="/bin/bash", capture_output=True, text=True
)
if result.returncode == 0:
print(" Upload successful.")
print(result.stdout)
else:
print(" Upload FAILED.")
print(result.stdout)
print(result.stderr)
def main():
DOWNLOADS_DIR.mkdir(exist_ok=True)
MIGRATION_OUTPUT_DIR.mkdir(exist_ok=True)
template = find_latest_paul_template()
template_dir = download_template(template)
output_path = run_migration(template_dir)
upload_to_docusign(output_path)
print(f"\nDone. DocuSign template JSON: {output_path}")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,143 @@
{
"name": "Employee Onboarding Form",
"description": "Migrated from Adobe Sign",
"documents": [
{
"documentBase64": "JVBERi0xLjMKJZOMi54gUmVwb3J0TGFiIEdlbmVyYXRlZCBQREYgZG9jdW1lbnQgKG9wZW5zb3VyY2UpCjEgMCBvYmoKPDwKL0YxIDIgMCBSIC9GMiAzIDAgUiAvRjMgNSAwIFIKPj4KZW5kb2JqCjIgMCBvYmoKPDwKL0Jhc2VGb250IC9IZWx2ZXRpY2EgL0VuY29kaW5nIC9XaW5BbnNpRW5jb2RpbmcgL05hbWUgL0YxIC9TdWJ0eXBlIC9UeXBlMSAvVHlwZSAvRm9udAo+PgplbmRvYmoKMyAwIG9iago8PAovQmFzZUZvbnQgL0hlbHZldGljYS1Cb2xkIC9FbmNvZGluZyAvV2luQW5zaUVuY29kaW5nIC9OYW1lIC9GMiAvU3VidHlwZSAvVHlwZTEgL1R5cGUgL0ZvbnQKPj4KZW5kb2JqCjQgMCBvYmoKPDwKL0NvbnRlbnRzIDEwIDAgUiAvTWVkaWFCb3ggWyAwIDAgNjEyIDc5MiBdIC9QYXJlbnQgOSAwIFIgL1Jlc291cmNlcyA8PAovRm9udCAxIDAgUiAvUHJvY1NldCBbIC9QREYgL1RleHQgL0ltYWdlQiAvSW1hZ2VDIC9JbWFnZUkgXQo+PiAvUm90YXRlIDAgL1RyYW5zIDw8Cgo+PiAKICAvVHlwZSAvUGFnZQo+PgplbmRvYmoKNSAwIG9iago8PAovQmFzZUZvbnQgL0hlbHZldGljYS1PYmxpcXVlIC9FbmNvZGluZyAvV2luQW5zaUVuY29kaW5nIC9OYW1lIC9GMyAvU3VidHlwZSAvVHlwZTEgL1R5cGUgL0ZvbnQKPj4KZW5kb2JqCjYgMCBvYmoKPDwKL0NvbnRlbnRzIDExIDAgUiAvTWVkaWFCb3ggWyAwIDAgNjEyIDc5MiBdIC9QYXJlbnQgOSAwIFIgL1Jlc291cmNlcyA8PAovRm9udCAxIDAgUiAvUHJvY1NldCBbIC9QREYgL1RleHQgL0ltYWdlQiAvSW1hZ2VDIC9JbWFnZUkgXQo+PiAvUm90YXRlIDAgL1RyYW5zIDw8Cgo+PiAKICAvVHlwZSAvUGFnZQo+PgplbmRvYmoKNyAwIG9iago8PAovUGFnZU1vZGUgL1VzZU5vbmUgL1BhZ2VzIDkgMCBSIC9UeXBlIC9DYXRhbG9nCj4+CmVuZG9iago4IDAgb2JqCjw8Ci9BdXRob3IgKGFub255bW91cykgL0NyZWF0aW9uRGF0ZSAoRDoyMDI2MDQxNDIzNTM0OC0wNCcwMCcpIC9DcmVhdG9yIChhbm9ueW1vdXMpIC9LZXl3b3JkcyAoKSAvTW9kRGF0ZSAoRDoyMDI2MDQxNDIzNTM0OC0wNCcwMCcpIC9Qcm9kdWNlciAoUmVwb3J0TGFiIFBERiBMaWJyYXJ5IC0gXChvcGVuc291cmNlXCkpIAogIC9TdWJqZWN0ICh1bnNwZWNpZmllZCkgL1RpdGxlICh1bnRpdGxlZCkgL1RyYXBwZWQgL0ZhbHNlCj4+CmVuZG9iago5IDAgb2JqCjw8Ci9Db3VudCAyIC9LaWRzIFsgNCAwIFIgNiAwIFIgXSAvVHlwZSAvUGFnZXMKPj4KZW5kb2JqCjEwIDAgb2JqCjw8Ci9GaWx0ZXIgWyAvQVNDSUk4NURlY29kZSAvRmxhdGVEZWNvZGUgXSAvTGVuZ3RoIDc5NAo+PgpzdHJlYW0KR2F0biRfLEIjQSY7S1kmTUVRKWNML0I0IVtVa0lCVGxkL3IlL0E3YkhPdSNIZ3RGTWBTLSpdZVEwXklTTzpJc1kiQktdUV1Ebz9VOElSInJSSyNMVCFRRzslL2Y2OFBgPWVPXCFmRztYL0wjLHJebmU7WDdhZy1nKF9AMCYhUDxBZj08RVBwNjFRUlovQjBxNF1WIWFjPFYqLkg+YXI+cGZnVFVVRTcqPjchdSp0bjMjT0tFLWhXbycyU09dWm0pUzlXanA+UWxxIlYwZlgwbVwtSzZfUj41dWVZJXJIaElIXW1zW25YMlEiRm9Xa18/KmgvLVhQP2taJTpsY2t1NV5pLFNPYltMUzhrZiQ7QDVBQ21sbzhZM1hyRixjcGdjJ11Ya0wnODYmJFB1Yyxbbk1eRlktWSZqY0p0SyVpRiMlISZQJiciUU8oNlQ9XUc2N0YwXk89LyRIVTIjKWxWR0VqKFFsYXVhbVo0bkRWWHFpX0VJOU1FZTFhKV4tUzdvZmpnSjB0U0xBOlxJKFFzLjlkWlg9cSJcb19XIzEkVHMhM0pFVTFWWl1GSWRFP1BZaXJKOkxTIT1WOXMwXWtrWTArPmkkR0Q7V05VJCVWakBRMz5tQkg5JjRpU1VbcExdJ0NsVmUvNDRYREA0bnNmZzNcJkwnNV5wI28+XU1KQV9GNVlEbGs2Ni0vdSgqaz1vNiNZXitoMlkkUGBvRDNnTHAsREtIVFZibSxvU0FgWFpQNGZRPy1OZy1DSidLOS4zRCFkI3BMWT0pOmBtYFtFTzEsTWNHPmUlYFBKSFZaT1FCSTU0UjN0Zi8mSkovVGxZbz8pYy84JUghIUVpNWt0aktdIiRXKEJfL1FfKm5WXiMmazArTVwnLCVpJCY7IjZMYSZxaTVsSkFZZmJtaUdtL1JaXnVsbFIlLHA6SjAxMHQlX1RNc2RMQD9zT1FOT2wvaVVbVUVJajk/VEstLUdwb15nPmcwPmAqXCVyTW82VmY9W2xNOFRWOlpvYE8lWD9nV1M9IihrLmM4MHVQZ2wpZ0MhUzJDdGdBfj5lbmRzdHJlYW0KZW5kb2JqCjExIDAgb2JqCjw8Ci9GaWx0ZXIgWyAvQVNDSUk4NURlY29kZSAvRmxhdGVEZWNvZGUgXSAvTGVuZ3RoIDY3Ngo+PgpzdHJlYW0KR2F0VXA6TityUCZCNCpjTUtfYWdiNFZvU15xQ0kzLkAkcEEsLzA8YDJZSSxnbTlPNkEsLy9KPEdQVThxJkh0VD9iOFQoJnJGVTRiIkBFKG9HTCgmNFAyWUdSSjw3aGYmOS83K3FoRnVhaThUbkUvajldYHFmajRwOC1CNmZWMHRsI2pocmhfYGFPTnRiWCsxKW1kIyIsRyF1LCFUOFtOQjtEN1FKUk1wNXI3c0hTTz4nNjxoZzA4KjtPUE8oJGNxL0xjQi5dV3JgLS89Zk8hSTJnR0ZkJUtoL3EuVUllLVFgZTdfayxEZ042aUwiVmZEW04/Ii4nSyklNUZBRVZtY1I4b28jOCpRdU81UjRKcjUwKDtSZDtJYyJGYClPVkJtJ2JtPStkYScpN0E+Sm9oLm0oS09AWlNPYyg+N1Vja0VqUiwwKW5YZXVhKmxsRVFsTFNzaEBeKUJfQEFTKHVrRDonRTBGcVo9VCZdXFFVXjFJZ1BRbyQocFJFWEA8ZXFjTUZpVEBYXHAzUls6K2tmWDJrPTZEPHFQZ2VVOHVfUldnaURPPk03P3U/WXVBYi5bRiV0bVUjNT42bHUzVzUvIXFWLTJkR05eJHRgWipFbDReOkA4Y3FrNG8raiduSGpJU0sjNmpTJUBeW2xDYi1hVi1gWUVYMCs4dGxjKmJhSmRSYkAmTy8oN14zaEVWMmlbKS44Zihmbk5IU0YnL2Itcl1fU25JRlRWaCs4LjMoZnBsRiMmJVhBKV5XLSsqY05aY2JnY2RhaSkkQ0wwIys4blFqXk5LJTNWMkcpMzI1Y2xXRypTOT5vWjhOSE5OLlJEXVtcUlFcRUxjPFwncFhyS0ItT2xwKjViUlssWTQ6K2h1IzdKVi1WYnFgOEReZXIrYCN+PmVuZHN0cmVhbQplbmRvYmoKeHJlZgowIDEyCjAwMDAwMDAwMDAgNjU1MzUgZiAKMDAwMDAwMDA2MSAwMDAwMCBuIAowMDAwMDAwMTEyIDAwMDAwIG4gCjAwMDAwMDAyMTkgMDAwMDAgbiAKMDAwMDAwMDMzMSAwMDAwMCBuIAowMDAwMDAwNTI1IDAwMDAwIG4gCjAwMDAwMDA2NDAgMDAwMDAgbiAKMDAwMDAwMDgzNCAwMDAwMCBuIAowMDAwMDAwOTAyIDAwMDAwIG4gCjAwMDAwMDExNjMgMDAwMDAgbiAKMDAwMDAwMTIyOCAwMDAwMCBuIAowMDAwMDAyMTEzIDAwMDAwIG4gCnRyYWlsZXIKPDwKL0lEIApbPDA0YTQ1NjEzNWZkMGRhOGQ1OWIzNTNiMTliMjFhZGVlPjwwNGE0NTYxMzVmZDBkYThkNTliMzUzYjE5YjIxYWRlZT5dCiUgUmVwb3J0TGFiIGdlbmVyYXRlZCBQREYgZG9jdW1lbnQgLS0gZGlnZXN0IChvcGVuc291cmNlKQoKL0luZm8gOCAwIFIKL1Jvb3QgNyAwIFIKL1NpemUgMTIKPj4Kc3RhcnR4cmVmCjI4ODAKJSVFT0YK",
"name": "OnboardingForm.pdf",
"fileExtension": "pdf",
"documentId": "1"
}
],
"recipients": {
"signers": [
{
"roleName": "SIGNER",
"recipientId": "1",
"routingOrder": "1",
"tabs": {
"textTabs": [
{
"tabLabel": "EmployeeName",
"required": "true",
"locked": "false",
"documentId": "1",
"pageNumber": "1",
"xPosition": "100",
"yPosition": "577"
}
],
"dateSignedTabs": [
{
"tabLabel": "StartDate",
"documentId": "1",
"pageNumber": "1",
"xPosition": "100",
"yPosition": "537"
}
],
"listTabs": [
{
"tabLabel": "Position",
"required": "true",
"documentId": "1",
"pageNumber": "1",
"xPosition": "100",
"yPosition": "497",
"listItems": [
{
"text": "Manager",
"value": "Manager"
},
{
"text": "Engineer",
"value": "Engineer"
},
{
"text": "Tech",
"value": "Tech"
},
{
"text": "HR",
"value": "HR"
}
]
}
],
"checkboxTabs": [
{
"tabLabel": "Benefits",
"required": "false",
"documentId": "1",
"pageNumber": "1",
"xPosition": "100",
"yPosition": "460"
}
],
"radioGroupTabs": [
{
"groupName": "CommuteGroup",
"documentId": "1",
"radios": [
{
"pageNumber": "1",
"xPosition": "100",
"yPosition": "420",
"value": "Car"
},
{
"pageNumber": "1",
"xPosition": "140",
"yPosition": "420",
"value": "Transit"
},
{
"pageNumber": "1",
"xPosition": "180",
"yPosition": "420",
"value": "Bike"
}
]
}
],
"signHereTabs": [
{
"tabLabel": "EmployeeSignature",
"documentId": "1",
"pageNumber": "2",
"xPosition": "100",
"yPosition": "460"
}
]
}
},
{
"roleName": "APPROVER",
"recipientId": "2",
"routingOrder": "2",
"tabs": {
"textTabs": [
{
"tabLabel": "HRNotes",
"required": "false",
"locked": "true",
"documentId": "1",
"pageNumber": "2",
"xPosition": "100",
"yPosition": "532"
}
],
"signHereTabs": [
{
"tabLabel": "HRSignature",
"documentId": "1",
"pageNumber": "2",
"xPosition": "300",
"yPosition": "460"
}
]
}
}
]
}
}

View File

@ -0,0 +1,106 @@
{
"name": "Employee Onboarding Form",
"description": "Migrated from Adobe Sign",
"documents": [
{
"documentBase64": "...",
"name": "OnboardingForm.pdf",
"fileExtension": "pdf",
"documentId": "1"
}
],
"recipients": {
"signers": [
{
"roleName": "SIGNER",
"recipientId": "1",
"email": "employee@example.com",
"name": "Employee",
"tabs": [
{
"tabLabel": "EmployeeName",
"tabType": "text",
"required": true,
"recipientIndex": 0,
"items": null,
"readOnly": false
},
{
"tabLabel": "StartDate",
"tabType": "dateSigned",
"required": true,
"recipientIndex": 0,
"items": null,
"readOnly": false
},
{
"tabLabel": "Position",
"tabType": "list",
"required": true,
"recipientIndex": 0,
"items": [
"Manager",
"Engineer",
"Tech",
"HR"
],
"readOnly": false
},
{
"tabLabel": "Benefits",
"tabType": "checkbox",
"required": false,
"recipientIndex": 0,
"items": null,
"readOnly": false
},
{
"tabLabel": "CommuteOption",
"tabType": "radio",
"required": false,
"recipientIndex": 0,
"items": [
"Car",
"Transit",
"Bike"
],
"readOnly": false
},
{
"tabLabel": "EmployeeSignature",
"tabType": "signHere",
"required": true,
"recipientIndex": 0,
"items": null,
"readOnly": false
}
]
},
{
"roleName": "APPROVER",
"recipientId": "2",
"email": "hr@example.com",
"name": "HR Representative",
"tabs": [
{
"tabLabel": "HRNotes",
"tabType": "text",
"required": false,
"recipientIndex": 1,
"items": null,
"readOnly": true
},
{
"tabLabel": "HRSignature",
"tabType": "signHere",
"required": true,
"recipientIndex": 1,
"items": null,
"readOnly": false
}
]
}
]
},
"status": "created"
}