new-site/scripts/formation/document_delivery.py
justin f8cd37ac8c Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers,
document generators, FCC compliance tools, Canada CRTC formation,
Ansible infrastructure, and deployment scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 06:54:22 -05:00

444 lines
14 KiB
Python

"""
document_delivery.py — Email formation documents to customers.
Sends a professional HTML email with attached formation documents
(Articles of Organization, EIN letter, operating agreement, etc.)
and updates the order status to 'delivered'.
Environment variables:
DATABASE_URL PostgreSQL connection string
SMTP_HOST SMTP server hostname
SMTP_PORT SMTP server port (default: 587)
SMTP_USER SMTP username / from address
SMTP_PASS SMTP password
Usage:
python -m formation.document_delivery <order_id>
"""
from __future__ import annotations
import email.mime.application
import email.mime.multipart
import email.mime.text
import json
import logging
import mimetypes
import os
import smtplib
import sys
from datetime import datetime, timezone
from pathlib import Path
import psycopg2
import psycopg2.extras
from .states import STATES
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
DATABASE_URL = os.environ.get("DATABASE_URL", "")
SMTP_HOST = os.environ.get("SMTP_HOST", "")
SMTP_PORT = int(os.environ.get("SMTP_PORT", "587"))
SMTP_USER = os.environ.get("SMTP_USER", "")
SMTP_PASS = os.environ.get("SMTP_PASS", "")
FROM_NAME = "Performance West"
FROM_EMAIL = SMTP_USER or "formations@performancewest.net"
LOG = logging.getLogger("formation.delivery")
# ---------------------------------------------------------------------------
# Email template
# ---------------------------------------------------------------------------
EMAIL_HTML_TEMPLATE = """\
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Your Business Has Been Filed</title>
</head>
<body style="margin:0; padding:0; background-color:#f4f4f7; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f7; padding: 40px 0;">
<tr><td align="center">
<!-- Header -->
<table width="600" cellpadding="0" cellspacing="0" style="background-color:#1a1a2e; border-radius:8px 8px 0 0; padding:30px 40px;">
<tr><td>
<h1 style="color:#ffffff; margin:0; font-size:24px; font-weight:600;">Performance West</h1>
<p style="color:#a0a0c0; margin:5px 0 0; font-size:14px;">Business Formation Services</p>
</td></tr>
</table>
<!-- Body -->
<table width="600" cellpadding="0" cellspacing="0" style="background-color:#ffffff; padding:40px;">
<tr><td>
<p style="font-size:16px; color:#333;">Dear {customer_name},</p>
<p style="font-size:16px; color:#333;">
Great news — your <strong>{entity_type}</strong> has been successfully
filed with the state of <strong>{state_name}</strong>.
</p>
<!-- Filing details box -->
<table width="100%" cellpadding="0" cellspacing="0" style="background-color:#f0f4ff; border-radius:8px; padding:24px; margin:24px 0;">
<tr><td>
<table width="100%" cellpadding="4" cellspacing="0">
<tr>
<td style="font-size:14px; color:#666; width:180px;">Entity Name</td>
<td style="font-size:14px; color:#1a1a2e; font-weight:600;">{entity_name}</td>
</tr>
<tr>
<td style="font-size:14px; color:#666;">State</td>
<td style="font-size:14px; color:#1a1a2e; font-weight:600;">{state_name}</td>
</tr>
<tr>
<td style="font-size:14px; color:#666;">Filing Number</td>
<td style="font-size:14px; color:#1a1a2e; font-weight:600;">{filing_number}</td>
</tr>
<tr>
<td style="font-size:14px; color:#666;">Confirmation Number</td>
<td style="font-size:14px; color:#1a1a2e; font-weight:600;">{confirmation_number}</td>
</tr>
<tr>
<td style="font-size:14px; color:#666;">Filed Date</td>
<td style="font-size:14px; color:#1a1a2e; font-weight:600;">{filed_date}</td>
</tr>
</table>
</td></tr>
</table>
<p style="font-size:16px; color:#333;">
Your formation documents are attached to this email.
</p>
<!-- Next steps -->
<h2 style="font-size:18px; color:#1a1a2e; margin:32px 0 16px;">Recommended Next Steps</h2>
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td style="padding:8px 0; font-size:15px; color:#333;">
<strong>1. Obtain an EIN</strong> — Apply for an Employer Identification Number
from the IRS. This is required to open a business bank account and file taxes.
{ein_note}
</td>
</tr>
<tr>
<td style="padding:8px 0; font-size:15px; color:#333;">
<strong>2. Operating Agreement</strong> — Prepare and sign an operating agreement
for your {entity_type}. This document outlines ownership, management structure,
and member responsibilities.
</td>
</tr>
<tr>
<td style="padding:8px 0; font-size:15px; color:#333;">
<strong>3. Open a Business Bank Account</strong> — Keep personal and business
finances separate. You'll need your Articles of Organization, EIN, and
operating agreement.
</td>
</tr>
<tr>
<td style="padding:8px 0; font-size:15px; color:#333;">
<strong>4. Business Licenses &amp; Permits</strong> — Check your local
city/county requirements for any additional licenses or permits.
</td>
</tr>
<tr>
<td style="padding:8px 0; font-size:15px; color:#333;">
<strong>5. Annual Reports</strong> — Most states require an annual or biennial
report. We'll send you a reminder when yours is due.
</td>
</tr>
</table>
<hr style="border:none; border-top:1px solid #e0e0e0; margin:32px 0;" />
<p style="font-size:14px; color:#666;">
If you have any questions about your filing or need additional services,
don't hesitate to reach out.
</p>
</td></tr>
</table>
<!-- Footer -->
<table width="600" cellpadding="0" cellspacing="0" style="background-color:#f4f4f7; padding:24px 40px;">
<tr><td>
<p style="font-size:13px; color:#999; margin:0;">
Performance West &middot; Business Formation &amp; Compliance Services
</p>
<p style="font-size:13px; color:#999; margin:4px 0 0;">
Email: formations@performancewest.net &middot; Phone: (307) 316-5620
</p>
<p style="font-size:12px; color:#bbb; margin:12px 0 0;">
This email and any attachments are intended solely for the named recipient.
If you received this in error, please delete it and notify the sender.
</p>
</td></tr>
</table>
</td></tr>
</table>
</body>
</html>
"""
# ---------------------------------------------------------------------------
# Database helpers
# ---------------------------------------------------------------------------
def _get_connection():
if not DATABASE_URL:
raise RuntimeError("DATABASE_URL environment variable is not set.")
return psycopg2.connect(DATABASE_URL)
def _fetch_order(conn, order_id: str) -> dict | None:
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
cur.execute("SELECT * FROM formation_orders WHERE order_id = %s", (order_id,))
row = cur.fetchone()
return dict(row) if row else None
def _mark_delivered(conn, order_id: str):
with conn.cursor() as cur:
cur.execute(
"""
UPDATE formation_orders
SET status = 'delivered',
delivered_at = NOW(),
updated_at = NOW()
WHERE order_id = %s
""",
(order_id,),
)
conn.commit()
# ---------------------------------------------------------------------------
# Email sending
# ---------------------------------------------------------------------------
def _build_email(
customer_email: str,
customer_name: str,
entity_name: str,
entity_type: str,
state_code: str,
filing_number: str,
confirmation_number: str,
filed_date: str,
documents: list[str],
ein: str = "",
) -> email.mime.multipart.MIMEMultipart:
"""Build the MIME email with HTML body and document attachments."""
state_name = STATES.get(state_code.upper(), {}).get("name", state_code)
# Entity type display name
type_display = {
"llc": "LLC",
"corporation": "Corporation",
"s_corp": "S Corporation",
}.get(entity_type.lower(), entity_type)
ein_note = ""
if ein:
ein_note = f"<br/><em>Your EIN ({ein}) has already been obtained and is included in your documents.</em>"
html_body = EMAIL_HTML_TEMPLATE.format(
customer_name=customer_name,
entity_type=type_display,
entity_name=entity_name,
state_name=state_name,
filing_number=filing_number or "Pending",
confirmation_number=confirmation_number or "N/A",
filed_date=filed_date or "N/A",
ein_note=ein_note,
)
msg = email.mime.multipart.MIMEMultipart("mixed")
msg["From"] = f"{FROM_NAME} <{FROM_EMAIL}>"
msg["To"] = customer_email
msg["Subject"] = f"Your {type_display} Has Been Filed — {entity_name}"
msg["Reply-To"] = FROM_EMAIL
# HTML body
html_part = email.mime.text.MIMEText(html_body, "html", "utf-8")
msg.attach(html_part)
# Attach documents
for doc_path in documents:
path = Path(doc_path)
if not path.exists():
LOG.warning("Document not found, skipping: %s", doc_path)
continue
content_type, _ = mimetypes.guess_type(str(path))
if content_type is None:
content_type = "application/octet-stream"
maintype, subtype = content_type.split("/", 1)
with open(path, "rb") as f:
attachment = email.mime.application.MIMEApplication(f.read(), _subtype=subtype)
attachment.add_header(
"Content-Disposition",
"attachment",
filename=path.name,
)
msg.attach(attachment)
LOG.info("Attached: %s (%s)", path.name, content_type)
return msg
def send_delivery_email(
order_id: str,
customer_email: str,
customer_name: str,
documents: list[str],
) -> bool:
"""
Send formation documents to a customer and update order status.
Args:
order_id: The formation order ID.
customer_email: Customer's email address.
customer_name: Customer's display name.
documents: List of file paths to attach.
Returns:
True if email sent successfully, False otherwise.
"""
if not SMTP_HOST:
LOG.error("SMTP_HOST not configured — cannot send email.")
return False
conn = _get_connection()
try:
order = _fetch_order(conn, order_id)
if not order:
LOG.error("Order not found: %s", order_id)
return False
entity_name = order.get("entity_name", "")
entity_type = order.get("entity_type", "llc")
state_code = order.get("state_code", "")
filing_number = order.get("filing_number", "")
confirmation_number = order.get("confirmation_number", "")
filed_at = order.get("filed_at")
ein = order.get("ein", "") or ""
filed_date = ""
if filed_at:
if isinstance(filed_at, str):
filed_date = filed_at[:10]
elif isinstance(filed_at, datetime):
filed_date = filed_at.strftime("%Y-%m-%d")
msg = _build_email(
customer_email=customer_email,
customer_name=customer_name,
entity_name=entity_name,
entity_type=entity_type,
state_code=state_code,
filing_number=filing_number,
confirmation_number=confirmation_number,
filed_date=filed_date,
documents=documents,
ein=ein,
)
LOG.info(
"Sending delivery email to %s for order %s (%s)...",
customer_email,
order_id,
entity_name,
)
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as smtp:
smtp.ehlo()
if SMTP_PORT != 25:
smtp.starttls()
smtp.ehlo()
if SMTP_USER and SMTP_PASS:
smtp.login(SMTP_USER, SMTP_PASS)
smtp.send_message(msg)
LOG.info("Email sent successfully to %s", customer_email)
# Mark order as delivered
_mark_delivered(conn, order_id)
LOG.info("Order %s marked as delivered", order_id)
return True
except smtplib.SMTPException as exc:
LOG.error("SMTP error sending to %s: %s", customer_email, exc)
return False
except Exception as exc:
LOG.error("Failed to send delivery email: %s", exc, exc_info=True)
return False
finally:
conn.close()
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main():
"""CLI entry point: deliver documents for a specific order."""
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(name)s] %(levelname)s %(message)s",
)
if len(sys.argv) < 2:
print("Usage: python -m formation.document_delivery <order_id>")
print()
print("Fetches order details from the database, builds a delivery email,")
print("and sends it with attached documents.")
sys.exit(1)
order_id = sys.argv[1]
if not DATABASE_URL:
print("Error: DATABASE_URL not set.", file=sys.stderr)
sys.exit(1)
if not SMTP_HOST:
print("Error: SMTP_HOST not set.", file=sys.stderr)
sys.exit(1)
conn = _get_connection()
try:
order = _fetch_order(conn, order_id)
if not order:
print(f"Error: Order {order_id} not found.", file=sys.stderr)
sys.exit(1)
customer_email = order.get("customer_email", "")
customer_name = order.get("customer_name", "")
if not customer_email:
print(f"Error: No customer_email on order {order_id}.", file=sys.stderr)
sys.exit(1)
# Gather document paths
docs_raw = order.get("documents")
if isinstance(docs_raw, str):
docs_raw = json.loads(docs_raw)
documents = docs_raw or []
finally:
conn.close()
success = send_delivery_email(order_id, customer_email, customer_name, documents)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()