new-site/scripts/workers/services/telecom/esign_helper.py
justin 40844b2aff Add generic eSign portal for all compliance document types
Reusable signing flow: service handler generates document → inserts
esign_records row → emails JWT link → client reviews PDF + signs →
API stores signature + resumes pipeline. Works for RMD, CPNI, CALEA,
499-A engagement, discontinuance, CRTC, and any future doc types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 10:45:37 -05:00

228 lines
7.7 KiB
Python

"""Generic eSign helper — create signing records and send signing links.
Usage from any service handler:
from scripts.workers.services.telecom.esign_helper import request_esign
request_esign(
conn=conn,
order_number="CO-ABC12345",
document_type="rmd",
document_title="RMD Certification Letter",
entity_name="Acme Telecom LLC",
customer_email="john@example.com",
customer_name="John Smith",
document_minio_key="compliance/CO-ABC12345/rmd_letter.pdf",
requires_perjury=True,
metadata={"frn": "0015341902"},
)
This will:
1. INSERT a row into esign_records (status = 'pending')
2. Generate a JWT portal token
3. Send a signing email with the portal link
"""
import logging
import os
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
logger = logging.getLogger("esign_helper")
def request_esign(
conn,
order_number: str,
document_type: str,
document_title: str,
entity_name: str,
customer_email: str,
customer_name: str = "",
document_minio_key: str = "",
requires_perjury: bool = False,
metadata: dict | None = None,
expires_hours: int = 72,
) -> int | None:
"""Create an esign record and email the signing link.
Returns the esign_records.id on success, None on failure.
"""
import json
from datetime import datetime, timedelta, timezone
try:
import jwt as pyjwt
except ImportError:
try:
import PyJWT as pyjwt # type: ignore
except ImportError:
logger.error("No JWT library available — cannot create esign link")
return None
secret = os.environ.get("CUSTOMER_JWT_SECRET", "changeme")
domain = os.environ.get("DOMAIN", "performancewest.net")
expires_at = datetime.now(timezone.utc) + timedelta(hours=expires_hours)
# 1. Upsert into esign_records (replace any existing pending record)
try:
with conn.cursor() as cur:
cur.execute(
"""INSERT INTO esign_records
(order_number, document_type, document_title, entity_name,
document_minio_key, document_metadata, requires_perjury,
status, expires_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, 'pending', %s)
ON CONFLICT (order_number, document_type)
WHERE status IN ('pending', 'signed')
DO UPDATE SET
document_title = EXCLUDED.document_title,
entity_name = EXCLUDED.entity_name,
document_minio_key = EXCLUDED.document_minio_key,
document_metadata = EXCLUDED.document_metadata,
requires_perjury = EXCLUDED.requires_perjury,
expires_at = EXCLUDED.expires_at,
updated_at = NOW()
RETURNING id""",
(
order_number,
document_type,
document_title,
entity_name,
document_minio_key,
json.dumps(metadata or {}),
requires_perjury,
expires_at,
),
)
row = cur.fetchone()
esign_id = row[0] if row else None
conn.commit()
except Exception as exc:
logger.error("Failed to insert esign record for %s: %s", order_number, exc)
conn.rollback()
return None
# 2. Generate JWT portal token
token = pyjwt.encode(
{
"order_id": order_number,
"order_type": document_type,
"email": customer_email,
},
secret,
algorithm="HS256",
)
sign_url = f"https://{domain}/portal/esign/?token={token}"
# 3. Send signing email
try:
_send_signing_email(
to_email=customer_email,
to_name=customer_name or entity_name,
entity_name=entity_name,
document_title=document_title,
sign_url=sign_url,
order_number=order_number,
)
logger.info(
"eSign link sent to %s for %s (%s)",
customer_email, order_number, document_type,
)
except Exception as exc:
logger.warning("Could not send eSign email for %s: %s", order_number, exc)
# Non-fatal — record exists, admin can resend
return esign_id
def _send_signing_email(
to_email: str,
to_name: str,
entity_name: str,
document_title: str,
sign_url: str,
order_number: str,
) -> None:
"""Send the signing invitation email."""
smtp_host = os.environ.get("SMTP_HOST", "co.carrierone.com")
smtp_port = int(os.environ.get("SMTP_PORT", "587"))
smtp_user = os.environ.get("SMTP_USER", "")
smtp_pass = os.environ.get("SMTP_PASS", "")
if not smtp_user or not smtp_pass:
logger.warning("SMTP credentials not configured — skipping eSign email")
return
subject = f"Action Required: Sign Your {document_title}"
body = f"""\
<div style="font-family:'Inter',system-ui,sans-serif;max-width:600px;margin:0 auto;color:#1f2937">
<div style="background:#1e3a5f;color:#fff;padding:24px 28px;border-radius:12px 12px 0 0">
<h1 style="margin:0;font-size:20px;font-weight:700">Document Ready for Signature</h1>
<p style="margin:6px 0 0;opacity:.8;font-size:14px">{entity_name}</p>
</div>
<div style="background:#fff;border:1px solid #e2e8f0;border-top:none;padding:28px;border-radius:0 0 12px 12px">
<p style="margin:0 0 16px;font-size:15px">Hi {to_name},</p>
<p style="margin:0 0 16px;font-size:15px">
Your <strong>{document_title}</strong> is ready for review and signature.
Please click the button below to review the document and provide your electronic signature.
</p>
<div style="text-align:center;margin:28px 0">
<a href="{sign_url}"
style="display:inline-block;background:#1e3a5f;color:#fff;padding:14px 36px;
border-radius:10px;font-weight:700;font-size:16px;text-decoration:none">
Review &amp; Sign Document
</a>
</div>
<p style="margin:0 0 12px;font-size:13px;color:#64748b">
This link expires in 72 hours. If it expires, contact us and we'll send a new one.
</p>
<hr style="border:none;border-top:1px solid #e2e8f0;margin:20px 0">
<p style="margin:0;font-size:12px;color:#9ca3af">
Performance West Inc. &mdash; (888) 411-0383<br>
Order: {order_number}
</p>
</div>
</div>"""
msg = MIMEMultipart("alternative")
msg["From"] = "Performance West <noreply@performancewest.net>"
msg["To"] = f"{to_name} <{to_email}>"
msg["Subject"] = subject
msg["Reply-To"] = "info@performancewest.net"
msg.attach(MIMEText(body, "html"))
with smtplib.SMTP(smtp_host, smtp_port, timeout=30) as server:
server.starttls()
server.login(smtp_user, smtp_pass)
server.send_message(msg)
def check_esign_status(conn, order_number: str, document_type: str) -> dict | None:
"""Check if a document has been signed. Returns the record dict or None."""
with conn.cursor() as cur:
cur.execute(
"""SELECT id, status, signed_at, signer_email, signature_type
FROM esign_records
WHERE order_number = %s AND document_type = %s
AND status IN ('pending', 'signed')
ORDER BY created_at DESC LIMIT 1""",
(order_number, document_type),
)
row = cur.fetchone()
if not row:
return None
return {
"id": row[0],
"status": row[1],
"signed_at": row[2],
"signer_email": row[3],
"signature_type": row[4],
}