#!/usr/bin/env python3 """DocuSign envelope automation for the quote-to-cash finish line. Flow: customer hits "Send Me This Configuration" -> signal watcher parses the config -> create_config_envelope() builds a DRAFT envelope from Sean's template, pre-filled with the config -> Sean reviews and sends (1 tap) -> docusign-status-watch cron drives sent -> signed with the never-stop routine. AUTH (not yet wired): requires the custom.docusign connector (oauth2_code, scope=signature). Once Sean completes the connector setup, scaffold the skill: /opt/hatch/skills/skill-creator/bin/scaffold-connector-skill --provider docusign then replace _api_token() below with the scaffolded skill's helper import. Until then every function raises DocusignNotConnected and callers must degrade gracefully (alert Sean, keep the manual path). DocuSign REST shapes below follow the official eSignature REST API v2.1. """ import json, urllib.request ACCOUNT_ID = "261419074" # Sean's new DocuSign account (2026-10-06) BASE_URI = None # discovered from /oauth/userinfo on first use class DocusignNotConnected(RuntimeError): pass def _api_token(): # Wire to the scaffolded DocuSign skill helper once custom.docusign exists. raise DocusignNotConnected( "custom.docusign connector not set up yet; Sean completes the OAuth link first") def _userinfo(token): req = urllib.request.Request( "https://account.docusign.com/oauth/userinfo", headers={"Authorization": "Bearer " + token}) return json.load(urllib.request.urlopen(req, timeout=30)) def _base_uri(token): global BASE_URI if BASE_URI: return BASE_URI info = _userinfo(token) for a in info.get("accounts", []): if a.get("account_id") == ACCOUNT_ID or a.get("is_default", False): BASE_URI = a["base_uri"] return BASE_URI raise RuntimeError("DocuSign account %s not in userinfo" % ACCOUNT_ID) def _call(token, method, path, data=None): body = json.dumps(data).encode() if data is not None else None req = urllib.request.Request(_base_uri(token) + path, data=body, method=method, headers={"Authorization": "Bearer " + token, "Content-Type": "application/json"}) return json.load(urllib.request.urlopen(req, timeout=60)) def create_config_envelope(template_id, customer, config, prefill): """Create a DRAFT envelope from template_id. customer: {name, email} config: parsed "Proposal configuration" dict (model, term, monthly, accessories...) prefill: {tabLabel: value} for the template's custom fields Returns (envelope_id, sender_view_url). Status is ALWAYS "created" (draft) — Sean sends it himself. """ token = _api_token() text_tabs = [{"tabLabel": label, "value": str(value)} for label, value in prefill.items()] payload = { "templateId": template_id, "status": "created", "emailSubject": "Your Stratix Systems equipment paperwork — %s" % customer.get("company", ""), "templateRoles": [{ "roleName": "Customer", "name": customer["name"], "email": customer["email"], "tabs": {"textTabs": text_tabs}, }], # DocuSign's own reminders run in parallel with our follow-up routine. "notification": { "useAccountDefaults": False, "reminders": {"reminderEnabled": True, "reminderDelay": "2", "reminderFrequency": "3"}, "expirations": {"expireEnabled": True, "expireAfter": "30", "expireWarn": "5"}, }, } res = _call(token, "POST", "/restapi/v2.1/accounts/%s/envelopes" % ACCOUNT_ID, payload) return res.get("envelopeId"), None def envelope_status(envelope_id): """Return the envelope's status: created/sent/delivered/completed/declined/voided.""" token = _api_token() res = _call(token, "GET", "/restapi/v2.1/accounts/%s/envelopes/%s" % (ACCOUNT_ID, envelope_id)) return res.get("status") def sender_view_url(envelope_id): """URL Sean opens to review and send the draft envelope.""" token = _api_token() res = _call(token, "POST", "/restapi/v2.1/accounts/%s/envelopes/%s/views/sender" % (ACCOUNT_ID, envelope_id), {"returnUrl": "https://app.docusign.com"}) return res.get("url")