""" 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 """ 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 = """\ Your Business Has Been Filed

Performance West

Business Formation Services

Dear {customer_name},

Great news — your {entity_type} has been successfully filed with the state of {state_name}.

Entity Name {entity_name}
State {state_name}
Filing Number {filing_number}
Confirmation Number {confirmation_number}
Filed Date {filed_date}

Your formation documents are attached to this email.

Recommended Next Steps

1. Obtain an EIN — Apply for an Employer Identification Number from the IRS. This is required to open a business bank account and file taxes. {ein_note}
2. Operating Agreement — Prepare and sign an operating agreement for your {entity_type}. This document outlines ownership, management structure, and member responsibilities.
3. Open a Business Bank Account — Keep personal and business finances separate. You'll need your Articles of Organization, EIN, and operating agreement.
4. Business Licenses & Permits — Check your local city/county requirements for any additional licenses or permits.
5. Annual Reports — Most states require an annual or biennial report. We'll send you a reminder when yours is due.

If you have any questions about your filing or need additional services, don't hesitate to reach out.

Performance West · Business Formation & Compliance Services

Email: formations@performancewest.net · Phone: (307) 316-5620

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.

""" # --------------------------------------------------------------------------- # 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"
Your EIN ({ein}) has already been obtained and is included in your documents." 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 ") 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()